<?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>Wed, 06 May 2026 14:24:20 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[Optimizing ML Workload Network Efficiency (Part I): Feature Trimmer]]></title>
            <link>https://medium.com/pinterest-engineering/optimizing-ml-workload-network-efficiency-part-i-feature-trimmer-ae20beb08d69?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/ae20beb08d69</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[efficiency]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 01 May 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-05-01T16:01:01.977Z</atom:updated>
            <content:encoded><![CDATA[<p>Guangtong Bai | Staff Software Engineer, Product ML Infrastructure*; Shantam Shorewala | Software Engineer II, Product ML Infrastructure*; Chi Zhang | Staff Software Engineer, AI Platform*; Neha Upadhyay | Software Engineer II, AI Platform*; Haoyang Li | Director, Product ML Infrastructure</p><p><em>*These authors contributed equally to this article.</em></p><h3>Background</h3><p>At Pinterest, our online ML serving systems employ a root-leaf architecture. On a high level, the architecture looks as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BRB0_pxzDH7wnHwiJplvVg.png" /><figcaption><em>Figure 1: Root-leaf Architecture of Online ML Serving Systems at Pinterest</em></figcaption></figure><p>In the diagram, “Client Service” is responsible for recommending organic or promoted Pins to users. In order to know if a given Pin is relevant to a particular user request, client service sends a score request to the online ML serving system to have the Pin scored by a bunch of ML models, each of which scores an aspect of “relevancy”.</p><p>The online ML serving system is composed of 2 parts:</p><ol><li><strong>Root:</strong> This component handles initial feature processing. Its responsibilities include retrieving necessary features from the feature store, performing required preprocessing, and distributing (fanning out) the scoring requests to the various leaf partitions.</li><li><strong>Leaf:</strong> This is where the actual model inference takes place, typically utilizing GPU machines. It is structured into multiple partitions, each of which hosts a related group of models, such as one production model and several experimental variants.</li></ol><p>What is flowing between the services are ML features. In this blog, we share how passing too many features from root to leaf created a network bottleneck and how we resolved it with Feature Trimmer.</p><h3>Motivation</h3><p>The root-leaf architecture provides us with significant benefits, namely:</p><ol><li><strong>Simplified Model Onboarding:</strong> New ML models can easily be onboarded for online serving by creating new leaf partitions, transparent to root and upstream clients.</li><li><strong>Reduced Feature Store QPS:</strong> The system minimizes RPCs to the feature store for fetching ML features by having all leaf partitions share a large in-memory feature cache in the root.</li><li><strong>Optimized Resource Utilization:</strong> Separating CPU (feature fetching, preprocessing) and GPU (model inference) workloads allows for optimized resource use, improving efficiency and reducing cost.</li></ol><p>However, this setup introduced a new challenge — <strong>the network bandwidth between root and leaf became a performance bottleneck on the online serving path; we had to scale the system based on network usage rather than compute</strong>. We observed this pressure in the Ads server on both the root and leaf partitions:</p><ul><li>On leaf partitions, peak network usage was significantly higher than peak GPU SM activity (see Figure 2). Consequently, the network bottleneck prevented us from fully utilizing the available GPU compute power.</li><li>On root, we had to use the network optimized AWS instance type m6in to ensure the server latency met our internal SLA.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fum5MdMGTpIw4Ho18ia7Qg.png" /><figcaption><em>Figure 2. Comparison of the network bandwidth usage vs GPU SM activity on a subset of the leaf partitions of the online ML server</em></figcaption></figure><p>That led to a straightforward idea: reduce the root-leaf network bandwidth usage to unlock immediate fleet downscaling and infrastructure savings. If we could cut bandwidth enough, we could also move the root from network-optimized m6in instances to standard m6i instances (about 20% cheaper), further reducing cost.</p><h3>Enable compression to reduce network usage</h3><p>The most direct way to reduce the root-leaf network bandwidth usage is to compress the requests between them.</p><p>This compression strategy is well-suited for the requests sent from the root to the leaf, which primarily carry ML features for multiple candidate Pins for a given user request. These requests are compressible for several reasons:</p><ol><li><strong>Feature Set Consistency:</strong> The set of features requested is identical across different candidate Pins, although the actual feature values vary.</li><li><strong>Feature Similarity:</strong> There are groups of features that share similar representations (e.g., last_x_pins_user_viewed and last_x_pins_user_clicked )</li><li><strong>Sparsity:</strong> Many features are sparse, containing numerous empty or zero values.</li></ol><p>After a few quick tests, we enabled lz4 compression in fbthrift (the RPC framework used by root and leaf) for root-leaf traffic. That reduced 20% root-leaf network usage, at the cost of 5% CPU usage increase and 5ms (~10%) p90 latency increase.</p><p>Compression was a solid early win, but it didn’t change the underlying problem: we were still shipping too much unused data. The bigger lever was to stop sending unused features altogether, which led to our “Send What You Use” approach.</p><h3>Send What You Use</h3><p>In our root–leaf architecture, the root is shared across many leaf partitions and must fetch ML features for all models. To minimize feature store QPS, the root fetches the union of features needed across models (per candidate Pin), stores them in an efficient in-memory cache, and then fans out the full feature set to each leaf model. Each model converts and uses only the features it needs; the rest are effectively discarded before inference.</p><p>This approach was acceptable in our prior architecture, where the same GPU host handled both feature fetching/preprocessing and local model inference. In that context, the unnecessary features only increased main memory usage, which was not a bottleneck on GPU machines. However, within the new root-leaf architecture, transmitting these unneeded features across the network introduces a significant efficiency problem.</p><p>If we could send only the required features and trim everything else, similar to C++’s “<a href="https://include-what-you-use.org/">include what you use</a>” header management tool removing unnecessary #include’s, we could potentially cut root-leaf network usage by ~50%. Like compression, this trades network savings for some additional CPU work and potential latency overhead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KSUZfYt1hiKiSnjjg1Plig.png" /><figcaption><em>Figure 3: Overview of the ML inference engine with root-leaf setup and feature trimming</em></figcaption></figure><p>To make this work, the root must know the exact feature list required by each leaf model. Since models refresh continuously, we also need to keep the feature allowlist on root in sync with the feature expectations of the latest model version on the leaf.</p><h4>Source of Truth: Model Signature</h4><p>The source of truth for which features are needed by a model is its <em>model signature</em>. Model signature defines the inputs and outputs of a model, similar to a function signature. As a version of a model finishes training, its model signature is exported as an extra file alongside the TorchScript artifact in the .pt archive file. Below is what a model signature looks like:</p><pre>❯ unzip -p model.pt archive/extra/module_info.json | jq<br>{<br>  &quot;input_names&quot;: [<br>    &quot;feature_id_1&quot;,<br>    &quot;feature_id_2&quot;,<br>    &quot;feature_id_3&quot;,<br>    ...<br>  ],<br>  &quot;output_names&quot;: [<br>    &quot;output_score_1&quot;,<br>    &quot;output_score_2&quot;<br>  ]<br>}</pre><p>When the leaf loads a specific model version from the .pt archive, it not only deserializes the weights from the TorchScript artifact, but also builds a feature converter from the model signature. The converter transforms input features from internal company format into native PyTorch tensors before passing them to the model. Because it knows the model’s inputs, it converts only the required features and discards the rest.</p><p>A crucial convention is that a model’s signature remains unchanged across different versions. If a signature modification is necessary — for instance, to introduce a new input feature — a new model is forked from the original. This practice is essential because it underpins the fallback mechanism for the versioned lookup feature of the Feature Trimmer, a topic discussed in detail later in the “<a href="https://docs.google.com/document/d/1qW_nwJjUoXOlb6naPZ_7hDx_Ow5jg4_01Lst7O2SH7s/edit?tab=t.0#bookmark=id.ptgrx7dlisgw">Versioned Lookups and Fallback</a>” section.</p><h4>Model Deploy Synchronization</h4><p>Feature Trimmer only works if the root knows exactly the features that the leaf model expects. That sounds simple until you factor in reality: models are refreshed frequently (hourly to daily), multiple models are shipped together as a “bundle”, and rollouts happen gradually (canary → prod, rolling deploys, occasional rollbacks).</p><p>This section explains how we keep the root up to date with what’s actually deployed on the leaf without adding heavy runtime dependencies or introducing brittle, manually managed configs.</p><p>At a high level, our approach is:</p><ul><li><strong>Treat the model signature as the source of truth </strong>which is exported as module_info.json.</li><li><strong>Publish signatures as lightweight artifacts </strong>that can be consumed by deployment pipelines.</li><li><strong>Aggregate per-model signatures into a per-bundle artifact</strong> that is deployed to the root alongside existing root configs.</li><li><strong>Use the same staged delivery semantics as model rollout </strong>(canary, automated canary analysis, prod, rollback), so trimmer config changes ride the same operational rails as everything else.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T26gZ-QGzGZiOL1AJ91P_w.png" /><figcaption><em>Figure 4: Root configurations artifact generation and delivery integrated with existing model deployment</em></figcaption></figure><p><strong>Publish module_info.json as a standalone artifact</strong></p><p>To make the model signature easy to ship and consume, we export module_info.json as a standalone file as part of the model training workflow, next to other model files (for example, alongside the model artifact and config files). This is important for synchronization as it ensures signatures are available before deployment, and available in a form that can be aggregated and deployed without any heavy runtime dependencies.</p><p><strong>Generate a bundle-level module_info mapping during bundle build</strong></p><p>In production, roots don’t serve a single model, they typically serve bundles containing multiple models (and sometimes multiple versions during a rollout window). So instead of deploying N per-model signatures independently, the bundle pipeline generates one bundle-level artifact that looks like:</p><pre>{<br>  &quot;model_A&quot;: [<br>    {<br>      &quot;version&quot;: &quot;1&quot;,<br>      &quot;input_names&quot;: [&quot;feature_id_1&quot;, &quot;feature_id_2&quot;, &quot;...&quot;],<br>      &quot;output_names&quot;: [&quot;score_1&quot;, &quot;...&quot;]<br>    },<br>    {<br>      &quot;version&quot;: &quot;2&quot;,<br>      &quot;input_names&quot;: [&quot;feature_id_1&quot;, &quot;feature_id_2&quot;, &quot;...&quot;],<br>      &quot;output_names&quot;: [&quot;score_1&quot;, &quot;...&quot;]<br>    }<br>  ],<br>  &quot;model_B&quot;: [<br>    {<br>      &quot;version&quot;: &quot;7&quot;,<br>      &quot;input_names&quot;: [&quot;feature_id_9&quot;, &quot;...&quot;],<br>      &quot;output_names&quot;: [&quot;score_x&quot;, &quot;...&quot;]<br>    }<br>  ]<br>}</pre><p>During the build step, the model deploy pipeline iterates over the model versions that will be shipped in the bundle.</p><ul><li>If a model version includes module_info.json, the pipeline parses it and records the signature.</li><li>If the signature is missing, the pipeline logs a warning and skips that version rather than failing the entire build. This keeps the system resilient while signature publishing is being rolled out across use cases.</li></ul><p>Finally, the bundle-level module_info file is packaged and uploaded together with other root configuration files, so the root receives one coherent “ configs” package.</p><p><strong>Deploy root configs through the same staged delivery flow</strong></p><p>Once the bundle build produces the root-config package, deployment follows the standard staged delivery pattern:</p><ol><li>Deploy root configs to Canary</li><li>Deploy model configs to Canary</li><li>Run Automated Canary Analysis (ACA)</li><li>Deploy root configs to Production</li><li>Deploy model configs to Production</li></ol><p>This is important because it integrates the feature trimmer into the existing model deployment system and ensures that the “root’s trimming view of the world” is updated using the same guardrails and rollback mechanics as other model changes.</p><p>We deploy root configs before rolling out new leaf model versions because the feature trimmer keys feature allowlists by model name + version. If a versioned request arrives without a matching allowlist, we skip trimming to avoid stale configs, which can cause a temporary rollout gap. To prevent this, we ship a backwards-compatible root artifact containing allowlists for both the current and pending versions. Discussed in more detail in a later section “<a href="https://docs.google.com/document/d/1qW_nwJjUoXOlb6naPZ_7hDx_Ow5jg4_01Lst7O2SH7s/edit?tab=t.0#bookmark=id.ptgrx7dlisgw">Versioned Lookups and Fallback</a>.”</p><p>On successful completion, the root hosts receive the bundle-level signature mapping at a known location on disk, and the trimmer can begin using it for per-model feature allowlisting.</p><h3>A Closer Look into Trimmer Internals</h3><h4>Feature Allowlist or Blocklist</h4><p>Once the root hosts have an idea of which features each model requires, we only keep the needed features in the fan-out request to leaf partitions. This <em>allowlist</em> approach, compared to a <em>blocklist </em>where we keep features <em>not</em> in the list, does not carry the burden of tracking all the features that might be in development or deprecated. Given the evolving nature of ML models and volume of experiments at Pinterest, the blocklist is significantly larger for any given model and it is probable that it will grow faster than the allowlist in the future.</p><h4>Concurrent Updates Across Model Bundles</h4><p>As mentioned earlier, a model bundle can contain multiple ML models. Additionally, the model bundles do not map 1:1 to the root cluster — each root cluster can receive traffic for multiple bundles. The bundles, each with their own module_info artifact, are deployed independently and often at different cadence. Further, we need to support independent rollbacks for even a single model bundle.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xH6sBT46q3B35u6LiTcbNQ.png" /><figcaption><em>Figure 5: Concurrent update handling for multiple bundles</em></figcaption></figure><p>A feature trimmer module is initialized on each root host when it comes online. This module maintains a consolidated, in-memory mapping from models to their versioned feature allowlist. Each trim request is efficiently serviced by looking up the model name and version within this consolidated map. The consolidated map uses the model name and version as nested keys for fast read access as follows.</p><pre>{<br>  &quot;model_A&quot;: {<br>&quot;version_N&quot;: [&quot;feature_id_1&quot;, &quot;feature_id_2&quot;, &quot;...&quot;],<br> &quot;version_M&quot;: [&quot;feature_id_1&quot;, &quot;feature_id_2&quot;, &quot;...&quot;],<br>  },<br>  &quot;model_B&quot;: {<br>&quot;version_N&quot;: [&quot;feature_id_3&quot;, &quot;feature_id_4&quot;, &quot;...&quot;],<br> &quot;version_K&quot;: [&quot;feature_id_4&quot;, &quot;feature_id_5&quot;, &quot;...&quot;],<br>  },<br>}</pre><p>This per-model feature allowlist map needs to be continuously refreshed as the model bundle is updated. Here is how it is managed:</p><ul><li><strong>Configuration:</strong> The root cluster is configured with the active model bundles, and the file path for each corresponding module_info.json is set using GFlags.</li><li><strong>Initial Loading: </strong>The feature trimmer module loads the content of each module_info.json file into an independent in-memory map.</li><li><strong>Monitor for Content Updates:</strong> A file watcher is attached to each module_info.json. Any content refresh triggers a reload of its contents into the in-memory map for the given model bundle.</li><li><strong>Consolidation:</strong> On initial loading or when any model bundle is refreshed, the module:<br> — Scans and merges <em>all</em> independent maps.<br> — Creates a new consolidated map.<br> — Atomically replaces the current active consolidated map with the new one.</li><li><strong>Concurrency Management w/ Read-Write Lock:<br> — </strong>Concurrent reads of the consolidated and independent maps are managed with a <strong>shared lock</strong>.<br> — Write access during the map replacement is managed with a <strong>unique lock</strong>.</li></ul><h4>Versioned Lookups and Fallback</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cL_LOfUl-3WWPjI4K07TOg.png" /><figcaption><em>Figure 6: Request flow for versioned lookup and fallback</em></figcaption></figure><p>Each scoring request sent to the root cluster must include the model name and optionally, the model version. If the version is omitted, it defaults to the <em>latest</em> version. The feature trimmer parses these fields to determine the version-specific feature allowlist for the requested model.</p><ul><li><strong>If no feature allowlist exists for the model,</strong> the request proceeds untrimmed.</li><li><strong>If both model name and version are specified and found,</strong> the specific version’s allowlist is used.</li><li><strong>If the model name is found but the version is either not specified or not found,</strong> the trimmer uses the latest version of the allowlist. This design choice is based on the assumption at Pinterest that the model signature remains consistent across versions, which also simplifies the deployment by avoiding the need to keep multiple versions in memory during a rolling deployment.</li></ul><p>The adoption of the feature trimmer is expected to reduce network bandwidth consumption for root-leaf connections. This places the trimmer on the critical failure path: failure to trim score requests can cause a significant spike in network bandwidth, potentially leading to cascading failures. Therefore, robust handling of artifact (module_info.json) corruption or deployment failures is essential.</p><p>We have implemented the following safeguards:</p><ul><li><strong>Initialization Failure Railguard:</strong> Upon Feature Trimmer module initialization, any failures while parsing the required module_infoartifacts are emitted to our observability dashboard and trigger an on-call alert. We specifically chose <em>not</em> to block host launch on initialization failure. This decision preserves our ability to respond to capacity-related incidents, especially if a deeper issue is affecting the Feature Trimmer module itself.</li><li><strong>Isolate Failures from a Single Model Bundle:</strong> The feature trimmer loads the module_info contents for each model bundle into a separate map in its memory. If a model bundle’s file gets corrupted on disk during an update, the feature trimmer keeps using the old, in-memory version for that bundle. Because each bundle has its own map, the feature trimmer can still successfully update the information for all the other model bundles.</li></ul><p>The fundamental assumption that the model signature is consistent across different model versions allows us to implement these precautions, ensuring the Feature Trimmer remains reliably operational even in the event of intermittent deployment failures.</p><h3>Efficiency Wins</h3><h4>Reduced Network Stress</h4><p>Ads root-leaf server setup was the biggest beneficiary of this launch. Figures 7 and 8 compare the network performance of the Ads root and leaf clusters post the launch of the feature trimmer module.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TTbTMvh4X4xfPFCW0kd81Q.png" /><figcaption><em>Figure 7. Comparison of the network bandwidth usage vs GPU SM activity on a subset of the leaf partitions of the online ML server after feature trimmer was enabled. The reduction in network usage allowed us to tune the cluster size and batch size config to improve the GPU utilization.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I7b2K-b91yh5ECdkQx0DBA.png" /><figcaption><em>Figure 8: Comparison of the network bandwidth consumption before and after launch of the feature trimmer on the Ads root cluster. It dropped from a peak of 4GBPS to &lt;1.5GBPS even after downsizing the root cluster by 27%.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jf7YwDcCKcseb1g7XdQWhQ.png" /><figcaption><em>Figure 9: Comparison of network bandwidth performance on Ads leaf partitions after the launch of the feature trimmer. The peak usage dropped from 1000–1200 MBPS in some clusters to &lt;200MBPS for all clusters.</em></figcaption></figure><p>Later, we also applied the feature trimmer to other use cases such as HomeFeed and Related Pins and saw latency and network reductions similar to Ads, amplifying the overall impact of this initiative. Figures 10 and 11 show the network savings in Homefeed Root and Leaf.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AnxwxlllewIjg78awFrUpw.png" /><figcaption><em>Figure 10: In our Homefeed Root cluster, outbound network usage dropped substantially from ~1.2–2.1 GB/s to ~0.45–1.1 GB/s</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IinF_XqRqT8lbbcrKI3CIw.png" /><figcaption><em>Figure 11: We saw 65–75% reduction in inbound network usage across Homefeed GPU leaf clusters</em></figcaption></figure><p>As a result, we reduced the Homefeed root cluster fleet size by 33% and are still working on rightsizing the Homefeed leaf clusters, unlocking significant infrastructure savings.</p><h4>Latency Improvement</h4><p>While the payload size reduction directly contributed to the network performance improvement, we also saw a reduction in CPU utilization on the root cluster and a reduction in both server-side and client-side root latency. We believe this is largely because a smaller payload leads to less CPU cycles spent on SerDe (serialization/deserialization). This additional latency headroom allowed Ads to save additional cost by trading some latency for cost and the remainder was used to unblock future experiments (see latency increases in late June).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FXcyPhk-4dQ7_C2ddQ27oQ.png" /><figcaption><em>Figure 12: Ads client (AdMixer) P90 latency dropped significantly as well, peaking above 90ms prior the launch to &lt;80ms peak after feature trimmer was enabled.</em></figcaption></figure><p>For our Related Pins surface, the model score latency p99 (ms) before the feature trimmer for most models sits around ~130–180 ms with frequent spikes above 200 ms. After the feature trimmer is enabled, the p99 baseline shifts down to roughly ~95–125 ms for most models, a notable ~25–30% drop in latency.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XGsEN7qozp-buHJbHmu6VA.png" /><figcaption><em>Figure 13: Feature Trimmer reduces Related Pins model p99 latency by ~25–30%. Note that the feature trimmer was not available for some models because they did not have a valid feature allowlist so these models still see the same peak latency post rollout.</em></figcaption></figure><h4>Cost Saving</h4><p>Based on the efficiencies realized in terms of network performance and client latency, we were able to resize the ML servers at Pinterest to realize significant cost savings:</p><ul><li>Ads was the biggest beneficiary of this project — the team could downsize the root cluster by 27% without any performance regression. On the leaf side, the network improvement allowed us to tinker with the batching logic to finetune GPU utilization without impacting any other metrics, representing roughly 5% of the total GPU capacity at the time.<br> — The latency reduction unblocked future improvements and marginally reduced the failures due to server timeouts — this led to a marginal 0.17% increase in revenue as well.</li><li>Across other use cases like Search and Notification, we saw approximately 45% and 65% drops in egress network throughput, with no material change in p99 latency. Because these clusters were initially network-bound, feature trimmer allowed us to move to more optimized instance types, resulting in ≥30% cost reduction for both.<br> — This realized an additional $0.98M in annual infrastructure cost from rightsizing the clusters</li></ul><p>Overall, this project saved over <strong>$4M</strong> in annual infrastructure costs for Pinterest while creating headroom to test bigger models and features without latency or network performance concerns. It effectively shifted the bottleneck from network to CPU cycles on the root cluster. This also allows the team to switch focus to optimizing the payload between the client and the root to further finetune the resource utilization end-to-end.</p><h3>Wrap Up</h3><p>Feature Trimmer successfully addressed a critical network bottleneck in Pinterest’s root-leaf ML serving architecture, moving beyond simple payload compression to implement a “Send What You Use” philosophy. By establishing the model signature as the source of truth for required features and deploying a robust, version-aware feature allowlisting system in sync with model rollouts, we significantly reduced the data volume passed between the root and leaf clusters. This optimization resulted in substantial network bandwidth reduction, improved client-side latency, and ultimately delivered significant cost savings.</p><p>In Part II of this blog series, we will shift focus to how request feature compression further optimizes the network connection between the client and the root. Keep an eye out for the next installment to discover how we achieve even greater efficiencies in our ML serving infrastructure.</p><h3>Acknowledgement</h3><p>This project would not have been possible without former team members Yiran Zhao and Queena Zhang’s early exploration and prototyping. We extend our sincere gratitude to the following individuals for their invaluable support in deploying Feature Trimmer into production: Miao Wang, Randy Carlson, Runze Su, Qifei Shen, and Tao Mo. We would also like to thank Nazanin Farahpour, Howard Nguyen, Bo Liu, Sihan Wang, Renjun Zheng and Zheng Liu for their helpful review of this blog post.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ae20beb08d69" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/optimizing-ml-workload-network-efficiency-part-i-feature-trimmer-ae20beb08d69">Optimizing ML Workload Network Efficiency (Part I): Feature Trimmer</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 Clicks to Conversions: Architecting Shopping Conversion Candidate Generation at Pinterest]]></title>
            <link>https://medium.com/pinterest-engineering/from-clicks-to-conversions-architecting-shopping-conversion-candidate-generation-at-pinterest-04cae5e1455b?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/04cae5e1455b</guid>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[monetization]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Mon, 27 Apr 2026 16:01:05 GMT</pubDate>
            <atom:updated>2026-04-27T16:01:05.242Z</atom:updated>
            <content:encoded><![CDATA[<p>Authors: Richard Huang | Machine Learning Engineer II; Yu Liu | Senior Machine Learning Engineer; Ziwei Guo | Senior Machine Learning Engineer; Andy Mao | Staff Machine Learning Engineer; Supeng Ge | Sr. Staff Machine Learning Engineer</p><h3>Introduction</h3><p>At Pinterest, conversion ads are crucial for matching users with products they are likely to purchase, boosting value for both users and advertisers¹. While conversion actions like checkout or add-to-cart are highly valuable, they are also technically challenging to optimize for. Because they occur offsite, conversion events are significantly sparser and noisier than onsite engagement signals. Historically, Pinterest’s shopping ads retrieval relied on engagement-based models. While effective for driving interaction, this system was not designed to optimize for lower-funnel conversions. This gap motivated us to build a dedicated candidate generation model tailored for conversions, aiming to surface higher-intent products and improve advertiser performance.</p><p>We launched our first shopping conversion model in 2023, achieving meaningful wins across both conversion and engagement, including a higher clickthrough rate (CTR). Further iterations in 2025 unlocked even stronger conversion value and improved Return on Ad Spend (RoAS) for our advertisers. This blog post documents our journey building this conversion candidate generation model, from its technical design and challenges to the key learnings of deploying it to our 600+ million monthly active users at Pinterest.</p><h3>Training Data Design</h3><p>Modeling conversion events is challenging. Unlike frequent, real-time onsite engagements (e.g., clicks), offsite conversions are reported by advertisers, making the data sparse, noisy, and delayed. Despite these difficulties, conversions remain one of the most valuable signals for a purchase intent model, offering a far stronger indication of advertiser value and true user intent than engagement alone. To address the inherent sparsity of conversions, we made several key design decisions:</p><ul><li><strong>Multi-Surface Model:</strong> We train a single model across all shopping surfaces (Homefeed, Related Pins, Search) to avoid fragmenting sparse conversion labels. At the same time, we incorporated surface-specific features to learn contextual differences between these surfaces.</li><li><strong>Dual Positive Signals:</strong> We supplement primary conversion signals with onsite engagement data (clicks, repins). This broadens data coverage, improving model generalization and ad funnel survival rates. To mitigate click data noise and decrease false positive clicks, we apply a log-based re-weighting function <em>w</em> based on the click duration:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rb9bNgDkjsxxfM8n42DdmQ.png" /></figure><p>where <em>t</em> is the non-negative click duration in seconds and <em>tₘₐₓ </em>is a tunable constant used to cap the re-weighting function.</p><ul><li><strong>Negative Sampling: </strong>On top of the existing in-batch negatives, we use ad impressions with no engagement as “harder negatives.” These samples can reflect the real distribution of served ads, exposing the model to a more representative inventory and promoting robust contrastive learning.</li></ul><p>In summary, our multi-task approach uses engagement prediction as an auxiliary task to stabilize training and boost performance. The crucial challenge is balancing the two tasks, ensuring the high-value conversion signal is not diluted by the more frequent engagement data.</p><h3>Feature Engineering</h3><p>At the core of our model are features that capture critical signals about our users and shopping catalog, grouped into two categories: User-side and Pin-side.</p><p><strong>User-side features</strong> are split into two types. First, context features capture a user’s real-time intent, which is vital for applications like Related Pins and Search. Examples include a subject Pin’s visual and GraphSAGE² embeddings. Second, preference &amp; historical features capture long-term interests for personalization. These include demographics, aggregated historical actions, and sequential data processed by a Transformer to create a user history embedding.</p><p><strong>Pin-side features </strong>take a multi-faceted approach, incorporating ID features, multi-modal/ content features for semantic understanding, and performance features tracking engagement.</p><p>This structured representation of users and Pins ensures an effective matching process, delivering both personalization and relevance in recommendations.</p><h3>Model Architecture and Loss function Design</h3><p>We use a two-tower model for retrieval, where user and Pin features are encoded separately, as there are no explicit user-Pin interaction features at this retrieval stage. To capture richer relationships among features within each tower, we employ DCN v2 (Deep &amp; Cross Network v2)³ as the foundation of our cross layers. This enhances the model’s capacity to model non-linear interactions and boosts retrieval quality. After the cross layers, the output embeddings are fed into the final MLP head(s).</p><p><strong>1. Parallel DCN v2 and MLP Cross Layers Architecture<br></strong>Early in our iterations, our cross-layer design was simple: a stacked architecture where DCN v2 cross network processed the input first, feeding its output into an MLP for dimension reduction. While efficient, we hypothesized that this sequential arrangement imposed a fundamental limit on the model’s learning capacity. To move beyond the sequential design, we designed a new parallel architecture by adding an MLP in parallel (see Figure 1). Its success stems from eliminating the primary drawback of a sequential flow: the information bottleneck. In the old setup, the MLP could only learn from features already processed by DCN v2, potentially losing valuable signals from the original input.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xz-HO7NGsT2s9MLBKD_cTg.png" /><figcaption>Figure 1: Sequential (left) and Parallel DCN v2 and MLP (right) Cross Layers Architecture</figcaption></figure><p>In contrast, our parallel design allows both the cross network and the deep network to learn directly and simultaneously from the same input features. This effectively decouples the learning tasks, the cross network captures richer and more expressive explicit feature interactions by applying cross operations that combine the original input with each successive layer’s output to construct higher-order feature crosses, while the 3-layer MLP learns implicit abstract patterns in parallel. Because the cross network always references the original input at every layer, it constructs higher-order feature crosses without any information being lost or distorted by a preceding MLP transformation. The combined output of both funnels yields a richer and more expressive representation, unlocking a higher level of performance.</p><p>We applied this design to both the Pin and query towers, validating it on the conversion task where it delivered a <strong>+11% gain in offline recall@1000</strong>⁴. Given its success in boosting core learning ability, particularly in its ability to surface stronger feature interactions while keeping a low latency for the retrieval task, this parallel architecture was subsequently <strong>adopted by all our production engagement retrieval models</strong>, achieving similar recall improvements as well as significant gains in online metrics.</p><p><strong>2. From a Multi-Head to a Unified Multi-Task Architecture<br></strong>In the first version of our model, we designed a multi-head structure to comprehensively make use of the conversion data and engagement data. To leverage the relative abundance of click data, we used a <strong>multi-head architecture</strong> with shared encoders followed by engagement and conversion heads. The engagement head helped stabilize shared parameters, while the conversion head preserved the unique purchase-intent signal. The two heads were trained simultaneously using a distinct sampled softmax loss (see Figure 2). To balance the influence of engagement data without diluting the conversion signal, different loss weights were applied. At serving time, only the conversion Pin and query embeddings were used.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q_E0bjg-oNEdv-PuhmctYA.png" /><figcaption>Figure 2: Multi-head architecture, 2023 (left) and Unified multi-task architecture, 2025 (right)</figcaption></figure><p>Through in-depth data analysis and several online experiments, we identified sparsity and noise in the conversion labels as one of the main bottlenecks of the previous model performance. To better stabilize query embeddings in regions of low conversion coverage, we moved from a multi-head architecture to a <strong>unified single-head multi-task architecture</strong> (cf. Figure 2). By merging the conversion and engagement heads, it allows the final embeddings to directly benefit from the multi-task optimization during serving.</p><p>Building on top of this, we also observed that conversion data at the Pin level exhibit high variance, making it challenging to reliably model purchase intent from Pin-level supervision alone. To address this, we introduce an<strong> advertiser-level loss function</strong> as an additional training objective, enabling the model to better capture conversion signals at a more stable and consistent granularity. With other model improvements and feature additions, we saw on average an<strong> increase of +42% recall@100</strong>⁴ for conversion tasks compared to our previous 2023 model.</p><h3>Conclusion</h3><p>In summary, our modeling journey in crafting the shopping conversion candidate generation was driven by the necessity of overcoming the inherent sparsity and noise of offsite conversion events. We addressed this through a sequence of loss design and architectural innovations. Key modeling decisions included the adoption of a unified model across all surfaces and the strategic use of conversion and click duration-weighted engagement data. Architecturally, we leveraged a highly effective Parallel DCN v2 and MLP Cross Layers architecture, and we progressed from an initial separate multi-head design to an unified multi-task architecture that introduced an advertiser-level matching objective to better align with the natural granularity of the conversion signal.</p><p>Introducing this new CG to production in 2023 delivered a <strong>2.3% increase in shopping conversion volume</strong> and a <strong>2.7% lift for the shopping impression to conversion rate</strong>. Beyond conversions, it also improved the Pinners’ shopping experience, with <strong>CTR increasing by 1.5%</strong> and <strong>CTR over 30 seconds rising by 2.2%</strong>. Building on this foundation, further iterations and refinements throughout 2025 continued to push the model’s performance forward, resulting in a <strong>3.1% improvement in RoAS</strong> for US shopping campaigns⁴, reinforcing that strong advertiser outcomes and a great Pinner experience are not at odds, but deeply intertwined.</p><h3>Acknowledgments</h3><p>Ads Retrieval: Yang Liu, Jay Ma (former), Peifeng Yin (former), Qingmengting Wang, Richika Sharan, Jitong Qi, Yufeng Su, Huiqin Xin</p><p>Ads Ranking: Weiwei Ying (former), Yiwei Sun (former), Aayush Mudgal, Hongda Shen, Han Sun</p><p>Ads Signal: Jiayin Jin (former), Daniel Yang (former), Chongyuan Xiang, Lakshmi Manoharan, Litian Tao, Siping Ji</p><p>Leadership: Alice Wu, Leo Lu (former), Ling Leng (former), Hari Venkatesan (former), Behnam Rezaei (former), Jamieson Kerns</p><h3>References</h3><p>¹ A. Mudgal, et al. 2024. <a href="https://medium.com/pinterest-engineering/evolution-of-ads-conversion-optimization-models-at-pinterest-84b244043d51">Evolution of Ads Conversion Optimization Models at Pinterest</a>. Pinterest Engineering Blog.</p><p>² W. L. Hamilton, et al. 2017. <a href="https://arxiv.org/pdf/1706.02216">Inductive Representation Learning on Large Graphs</a>. In NIPS.</p><p>³ R. Wang, et al. 2020. <a href="https://arxiv.org/pdf/2008.13535">DCN V2: Improved Deep &amp; Cross Network and Practical Lessons for Web-scale Learning to Rank Systems</a>. WWW ’21: Proceedings of the Web Conference 2021.</p><p>⁴ Pinterest Internal Data, US, 2023 to 2025.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=04cae5e1455b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/from-clicks-to-conversions-architecting-shopping-conversion-candidate-generation-at-pinterest-04cae5e1455b">From Clicks to Conversions: Architecting Shopping Conversion Candidate Generation at Pinterest</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[Smarter URL Normalization at Scale: How MIQPS Powers Content Deduplication at Pinterest]]></title>
            <link>https://medium.com/pinterest-engineering/smarter-url-normalization-at-scale-how-miqps-powers-content-deduplication-at-pinterest-4aa42e807d7d?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/4aa42e807d7d</guid>
            <category><![CDATA[pinner-experience]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[eng-culture]]></category>
            <category><![CDATA[pinterest]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Mon, 20 Apr 2026 16:01:04 GMT</pubDate>
            <atom:updated>2026-04-20T16:01:04.436Z</atom:updated>
            <content:encoded><![CDATA[<p>Shanhai Liao | Senior Software Engineer, Content Acquisition and Media Platform; Di Ruan, | Senior Staff Software Engineer, Content Acquisition and Media Platform; Evan Li, | Senior Engineering Manager, Content Acquisition and Media Platform</p><h3>Introduction</h3><p>Accurate content understanding underpins Pinterest’s ability to drive distribution and engagement. This requires deep insight not just into the image itself, but also the outbound links or items to which those images point. At the foundation of this process lies a deceptively simple problem: URL normalization.</p><p>When Pinterest ingests content from millions of merchant domains, the same product page often appears under many different URLs. A single pair of shoes might be referenced by dozens of URL variations — each one decorated with different tracking parameters, session tokens, or analytics tags. While downstream systems can eventually deduplicate by content identity, the inability to recognize these duplicates at the URL level means every variation is independently fetched, rendered, and processed. At scale, this redundant ingestion and processing represents a significant waste of computational resources — rendering the same page dozens of times simply because its URLs differ in irrelevant parameters.</p><p>Item canonicalization — ensuring that identical items represented by different URLs are unified — is critical for organizing shopping catalogs and presenting a consistent experience to users. For many partners, a provided item ID determines canonical identity, but in its absence, the onus falls to advanced URL normalization to deduplicate effectively.</p><p>This post details the technical journey behind the <strong>Minimal Important Query Param Set (MIQPS)</strong> algorithm: a system that automatically learns which URL parameters matter for content identity, enabling dynamic and precise URL normalization at scale.</p><h3>Background: The URL Normalization Challenge</h3><p>Consider a typical product URL from an e-commerce site:</p><pre>https://example.com/shoes?id=42&amp;color=red</pre><p>This URL identifies a specific product variant. But in practice, the same product page is often reached through URLs like:</p><pre>https://example.com/shoes?id=42&amp;color=red&amp;utm_source=facebook&amp;session=abc123<br>https://example.com/shoes?id=42&amp;color=red&amp;ref=pinterest&amp;click_id=xyz<br>https://example.com/shoes?id=42&amp;color=red&amp;tracking=campaign_spring</pre><p><strong>Figure 1: The URL duplication problem.</strong> Multiple URLs with different tracking parameters all resolve to the same product content.</p><figure><img alt="Diagram showing three different URLs with different query parameters all pointing to the same product page content, illustrating the URL duplication problem." src="https://cdn-images-1.medium.com/max/1024/1*04DW89j1STxyHKOzzaY4NA.png" /><figcaption><em>Caption: Figure 1: Multiple URLs with different query parameters all point to the same underlying product page.</em></figcaption></figure><p>The parameters utm_source, session, ref, click_id, and tracking are all <strong>neutral </strong>- they don’t change the content of the page. Meanwhile, id and color are <strong>non-neutral</strong> - they determine which product and variant are displayed.</p><p>The challenge is distinguishing between the two. For well-known e-commerce platforms, this can be solved with curated rules. Shopify URLs, for example, use variants as the key product differentiator. Salesforce Commerce Cloud uses parameters like start, sz, prefn1, and prefv1. For these platforms, static allowlists are sufficient.</p><p>But Pinterest ingests content from a large number of domains, operating on a wide variety of platforms.</p><p>For this long tail of domains, URL parameter conventions vary wildly. Static rules cannot scale to cover them all. We need a dynamic, data-driven approach.</p><h3>The MIQPS Algorithm</h3><p>The core insight behind MIQPS is straightforward: <strong>if removing a query parameter changes the content of a page, that parameter is important; if it doesn’t, the parameter is noise and can be safely stripped.</strong> Crucially, this analysis runs independently per domain — each merchant site gets its own MIQPS map, because the same parameter name can be meaningful on one domain and irrelevant on another.</p><p>The algorithm operates in three steps.</p><h4>Step 1: Collect the URL Corpus</h4><p>As Pinterest’s content ingestion pipeline processes URLs from domains, the system accumulates a corpus of observed URLs per domain. This corpus is stored durably and represents a snapshot of all the URL variations seen for a given domain. It serves as the input to the MIQPS analysis.</p><h4>Step 2: Group URLs by Query Parameter Pattern</h4><p>Not all URLs from a domain share the same set of query parameters. A product page URL might carry {id, color, utm_source} while a category page might carry {category, page, sort}. Analyzing them together would be meaningless.</p><p>Moreover, the same parameter name can play different roles depending on its context. Consider the parameter `ref`: on a product page URL like `example.com/product? id = 42 &amp; ref = homepage`, `ref` is purely a tracking parameter and is neutral - removing it doesn’t change the product displayed. But on a comparison page URL like `example.com/compare? ref=99`, the same `ref` parameter identifies which items to compare and is non-neutral. By grouping URLs by their full parameter pattern, the algorithm evaluates each parameter within its specific context, correctly classifying it as neutral in one pattern and non-neutral in another.</p><p>To address this, the algorithm groups URLs by their <strong>query parameter pattern</strong> — the sorted set of parameter names present in the URL. For example:</p><p>To address this, the algorithm groups URLs by their <strong>query parameter pattern</strong> — the sorted set of parameter names present in the URL. For example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AipLT77mJ6OY2li1bYIRwQ.png" /></figure><p>URLs sharing the same query pattern are grouped together. The top <em>K</em> patterns by URL count are selected for analysis, focusing computational resources on the patterns that matter most.</p><h3>Step 3: For Each Pattern, Test Each Parameter</h3><p>For each query parameter within a pattern, the algorithm determines whether it is neutral or non-neutral through empirical testing:</p><p>1.<strong> Sample:</strong> Select up to <em>S</em> URLs with distinct values for the parameter under test.</p><p>2. <strong>Compare:</strong> For each sampled URL, compute the <strong>content ID</strong> — a fingerprint derived from the page’s rendered visual content — for both:<br> — The original URL (with the parameter present)<br> — A modified URL (with the parameter removed)</p><p>3.<strong> Classify:</strong> If removing the parameter changes the content ID in at least <em>T</em>% of samples, the parameter is classified as <strong>non-neutral</strong> (important). Otherwise, it is <strong>neutral</strong> (safe to drop).</p><p>The content ID is a hash of the page’s visual representation, meaning two URLs that render the same visible content will produce the same content ID, even if their underlying HTML differs slightly. This particular fingerprinting approach leverages Pinterest’s in-house page rendering infrastructure, which is tailored to our content pipeline. The core MIQPS algorithm, however, is agnostic to how the content fingerprint is produced — it only requires a function that returns the same identifier for the same page content. Third parties looking to adopt a similar approach could substitute alternatives such as DOM tree hashing, HTTP response body checksums, or even simpler heuristics like comparing the `&lt;title&gt;` and Open Graph metadata across URL variants. The key principle remains the same: compare some representation of the page content with and without each parameter to determine its importance.</p><p>A natural question is: why not simply use the **canonical URL** declared in the page’s HTML (via the `&lt;link rel=”canonical”&gt;` tag) to resolve duplicates? If the merchant provides a canonical URL, two variant URLs pointing to the same product should share the same canonical, making deduplication trivial. In practice, however, canonical URLs are unreliable at scale. Many merchant sites omit them entirely, set them incorrectly (e.g., pointing every page to the homepage), or include tracking parameters in the canonical URL itself. Because we cannot assume canonical URLs are present or correct across the long tail of merchant domains, MIQPS uses visual content comparison as a ground-truth signal that works regardless of how well-maintained a site’s metadata is.</p><h3>Algorithm Parameters</h3><p>The behavior of the MIQPS algorithm is governed by a small set of tunable parameters:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hwujQfq8faQhSV4A90jh4Q.png" /></figure><p>Two additional design choices make the algorithm practical at scale:</p><ul><li><strong>Early exit optimization:</strong> If the mismatch rate already exceeds <em>T</em>% after <em>N</em> successful tests, we stop testing that parameter early. This avoids unnecessary page rendering calls for parameters that are clearly non-neutral.</li><li><strong>Conservative default:</strong> When fewer than <em>N</em> sample URLs are available for a parameter, it is treated as non-neutral by default. The system errs on the side of keeping parameters rather than dropping ones that might matter.</li></ul><h3>Putting It Together</h3><p><strong>Figure 2: The MIQPS computation pipeline.</strong></p><p>The output of this pipeline is a <strong>MIQPS map</strong>: a mapping from each query parameter pattern to the set of non-neutral parameters within that pattern. This map is published to a configuration store and consumed at runtime during URL normalization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CIlpLWHKpPdWG224AbX3SA.png" /></figure><h3>Multi-Layer Normalization Strategy</h3><p>MIQPS does not operate in isolation. In production, URL normalization combines <strong>static rules</strong> with the <strong>dynamically computed MIQPS</strong>. Static rules capture known conventions — curated allowlists for recognized e-commerce platforms and regex patterns for widely used parameter naming schemes. These rules handle cases where we already have high confidence about which parameters matter.</p><p>MIQPS complements these static rules by covering the long tail of domains where no predefined rules exist. A URL parameter is kept if it is matched by either the static rules or the MIQPS non-neutral set. Only parameters that pass neither check are stripped. This combination ensures broad coverage: static rules provide immediate, reliable handling for known platforms, while MIQPS dynamically adapts to everything else.</p><h3>Anomaly Detection: Guarding Against Regressions</h3><p>Computing MIQPS is inherently dependent on external page rendering. Pages can change, rendering infrastructure can have transient issues, and a domain’s URL structure can shift between analysis runs. Without safeguards, a bad MIQPS computation could cause the system to start dropping parameters that are actually important — leading to content deduplication errors and degraded catalog quality.</p><p>To address this, the system includes an anomaly detection layer that compares each newly computed MIQPS against the previously published version. The comparison follows a set of conservative rules:</p><ul><li><strong>Parameter removed from non-neutral set (anomaly):</strong> If a parameter that was previously classified as non-neutral is now classified as neutral, the pattern is flagged as anomalous. This is the dangerous case — it means we would start stripping a parameter that we previously determined was important.</li><li><strong>Parameter added to non-neutral set (not anomalous):</strong> If a previously neutral parameter is now classified as non-neutral, this is not considered an anomaly. It simply means we discovered a new important parameter, and the worst case is keeping slightly more parameters than necessary.</li><li><strong>Pattern removed entirely (not anomalous):</strong> If a query pattern from the previous MIQPS is absent in the new one, this is not flagged. Patterns can naturally disappear as a domain’s URL structure evolves.</li></ul><p>If more than <em>A</em>% of existing patterns are flagged as anomalous, the entire MIQPS update is rejected and the previous version is retained. This ensures the system never regresses — it errs on the side of over-keeping parameters rather than accidentally dropping ones that affect content identity.</p><h3>System Architecture and Integration</h3><p>The MIQPS system fits into Pinterest’s content processing pipeline as follows:</p><p><strong>Figure 3: End-to-end system architecture.</strong></p><figure><img alt="System architecture diagram with three phases: content ingestion produces a URL corpus, offline MIQPS computation uses page rendering for content ID comparison with anomaly detection before publishing, and the URL normalization phase where the URL processor reads MIQPS from the config store." src="https://cdn-images-1.medium.com/max/1024/1*f_UtyminfV-Y6Z5Mnyu16Q.png" /><figcaption><em>Figure 3: End-to-end system architecture. The content ingestion pipeline produces a URL corpus per domain. An offline job analyzes parameter importance via content ID comparison, then publishes the MIQPS to a config store after anomaly checks. The URL processor reads the MIQPS at runtime to normalize URLs during content processing.</em></figcaption></figure><p>The architecture has three distinct phases:</p><ul><li><strong>Content Ingestion:</strong> As URLs are processed from domains, the system writes each unique URL to a per-domain corpus stored in S3. This happens continuously as part of normal content processing.</li><li><strong>MIQPS Computation:</strong> After a content processing cycle completes for a domain, an offline job is triggered. This job downloads the URL corpus, runs the MIQPS algorithm (grouping, sampling, content ID comparison), performs anomaly detection, and publishes the result to both a config store (for runtime consumption) and S3 (for archival and debugging).</li><li><strong>URL Normalization:</strong> At runtime, the URL processor loads the MIQPS map from the config store at initialization. For each URL it processes, it looks up the query pattern, retrieves the non-neutral parameter set, and strips all parameters not matched by any of the four normalization layers.</li></ul><p>This separation of concerns means the expensive content ID comparison happens offline and asynchronously, while runtime URL normalization is a fast, in-memory lookup.</p><p>An alternative design would be to determine parameter importance **in realtime** — rendering the page with and without each parameter at the moment a URL is first encountered. This would eliminate staleness entirely and provide immediate coverage for newly discovered domains. However, we chose the offline approach for several reasons:</p><p>- <strong>Latency</strong>: Each content ID computation requires rendering a full page, which takes seconds. Testing every parameter in a URL would multiply this cost, adding unacceptable latency to the content processing pipeline.</p><p>- <strong>Cost</strong>: Offline analysis scales with the number of domains, while realtime analysis would scale with the number of URLs — orders of magnitude more expensive.</p><p>- <strong>Reliability</strong>: Transient rendering failures in an offline job are isolated and retryable. In a realtime path, they would directly block content processing.</p><p>In practice, the offline approach is a natural fit because URL parameter conventions change infrequently — on the order of weeks or months. The small amount of staleness between computation cycles is an acceptable tradeoff for the massive savings in cost, latency, and operational complexity.</p><h3>Conclusion</h3><p>URL normalization may seem like a mundane infrastructure problem, but at Pinterest’s scale — with a large number of domains and billions of URLs — getting it right has outsized impact on content quality.</p><p>The MIQPS algorithm brings several key properties to this challenge:</p><ul><li><strong>Dynamic and data-driven:</strong> MIQPS automatically adapts to each domain’s URL conventions without requiring manual configuration or domain-specific rules. As a domain’s URL structure evolves, the algorithm discovers new patterns and adjusts accordingly.</li><li><strong>Layered and defense-in-depth:</strong> The multi-layer normalization strategy combines static allowlists, regex patterns, and dynamically computed MIQPS. Each layer catches a different class of parameters, and a parameter only needs to match one layer to be preserved.</li><li><strong>Conservative and regression-resistant:</strong> The anomaly detection system ensures that MIQPS updates never regress — previously important parameters cannot be silently dropped. The system consistently errs on the side of keeping parameters rather than stripping them.</li><li><strong>Scalable and cost-efficient:</strong> By grouping URLs by pattern, focusing on the top <em>K</em> patterns, and using early exit optimizations, the algorithm keeps computational costs manageable even across hundreds of thousands of domains.</li></ul><p>By aligning normalization strategies with proven content identity signals, MIQPS ensures every unique item or experience is surfaced cleanly — improving search and recommendations, downstream catalog management, and ultimately the user experience.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4aa42e807d7d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/smarter-url-normalization-at-scale-how-miqps-powers-content-deduplication-at-pinterest-4aa42e807d7d">Smarter URL Normalization at Scale: How MIQPS Powers Content Deduplication at Pinterest</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[Finding zombies in our systems: A real-world story of CPU bottlenecks]]></title>
            <link>https://medium.com/pinterest-engineering/finding-zombies-in-our-systems-a-real-world-story-of-cpu-bottlenecks-ea4722e552eb?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea4722e552eb</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Wed, 15 Apr 2026 16:01:04 GMT</pubDate>
            <atom:updated>2026-04-15T16:01:04.668Z</atom:updated>
            <content:encoded><![CDATA[<p>Vaibhav Shankar; Staff Software Engineer | Raymond Lee; Staff Software Engineer | Chia-Wei Chen; Staff Software Engineer | Shunyao Li; Sr. Software Engineer | Yi Li; Staff Software Engineer | Ambud Sharma; Principal Engineer | Saurabh Vishwas Joshi; Principal Engineer | Charles-A. Francisco; Senior Engineer | Karthik Anantha Padmanabhan; Director, Engineering | David Westbrook; Sr. Manager, Engineering</p><p>One day in early 2025, the Kubernetes platform team at Pinterest (<a href="https://medium.com/pinterest-engineering/pincompute-a-kubernetes-backed-general-purpose-compute-platform-for-pinterest-8ad408df2d6f">PinCompute</a>) got a ping from our partners on the ML platform team. Their <a href="https://medium.com/pinterest-engineering/ray-infrastructure-at-pinterest-0248efe4fd52">Ray-based training jobs</a> , which often take hours of computation on expensive GPU hardware, were crashing. Not every time, but often enough that it was becoming noticeable. Their logs indicated that their distributed training jobs were seeing intermittent loss of network connectivity, and that ultimately caused their jobs to crash. Their ask was simple:</p><ol><li>Why is this happening?</li><li>Can you please make it stop?</li></ol><p>What started there led to a more than three-month-long investigation and a great lesson in profiling performance bottlenecks. Read on to learn from our fun story about CPU bottlenecks, AWS network drivers, and yes, how we discovered Zombies in our system!</p><h3>Background: Ray at Pinterest</h3><p>At Pinterest, Ray has risen as the backbone of our next-gen ML training and inference. Over the past few years, it has enabled us to scale systems, accelerate experimentation, and significantly boost the performance of models powering our diverse ML workloads.</p><p>We have previously shared deep dives on our progress, including: <strong>Ray Infrastructure</strong> (provisioning ray cluster on in-house K8s clusters at scale [<a href="https://medium.com/pinterest-engineering/ray-infrastructure-at-pinterest-0248efe4fd52">blog</a>]), <strong>Batch Inference with Ray</strong> (scaling to hundreds of nodes [<a href="https://medium.com/pinterest-engineering/ray-batch-inference-at-pinterest-part-3-4faeb652e385">blog</a>][<a href="https://www.youtube.com/watch?v=HDSy09hrm2I">talk</a>]), <strong>Ray for Training</strong> (distributed dataloaders and throughput optimization [<a href="https://www.youtube.com/watch?v=yqVLRONwDJs">talk</a>]), and <strong>Last-Mile Data Processing</strong> (reducing experimentation cycles [<a href="https://medium.com/pinterest-engineering/last-mile-data-processing-with-ray-629affbf34ff">blog 1</a>][<a href="https://medium.com/pinterest-engineering/scaling-pinterest-ml-infrastructure-with-ray-from-training-to-end-to-end-ml-pipelines-4038b9e837a0">blog 2</a>]).</p><p>Today, we run more than half of the offline ML workload company-wide on Ray, provisioning tens of thousands of Ray clusters per month, a feat made possible only by a robust Kubernetes environment.</p><h4><strong>Network Model &amp; Challenges</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LI6XdmCMfpirO6JDoESaWA.png" /><figcaption><em>Figure 1: Ray architecture at Pinterest</em></figcaption></figure><p>What makes the network stability challenging lies in Ray’s unique network model.</p><p>Ray operates as a highly “network-active” system. A Ray cluster generates constant, intensive inter-pod gRPC traffic that is fundamental to the cluster’s operation, with the following two distinct layers:</p><ul><li><strong>Control Plane:</strong> Handles stateful operations, such as node health check, task submission, actor scheduling, and the maintenance of Object References.</li><li><strong>Data Plane:</strong> Handles the high-volume transfer of values within the Object Store. Our Large-scale ML training relies on this plane to move data rapidly between nodes.</li></ul><p>Because this traffic is highly distributed and latency-sensitive, the impact of network instability is often non-deterministic, manifesting across various components of Ray Cluster:</p><ul><li><strong>Job Hanging:</strong> Caused by actor state corruption following brief network interruptions. [<a href="https://www.google.com/search?q=link&amp;authuser=1">github issue</a>]</li><li><strong>ObjectFetchTimedOutError</strong> / <strong>ObjectLossError</strong></li><li><strong>ActorDiedError</strong></li><li>Node failed the health check and crashed</li><li>…</li></ul><p>All of these occurrences resulted in one common outcome: our Ray Training jobs would crash (some use cases with &gt; 25% Success Rate drop), resulting in loss of expensive compute hours and significant slowdown in Model building and experimentation. After grinding for over a month seeking solutions for individual issues in the Ray stack, the ML Platform team realized it was necessary to turn our attention to look for more lower level network issues with our friends from the PinCompute team.</p><h3>Symptom 1: Network driver resets</h3><p>At Pinterest, our Kubernetes clusters are backed by AWS EC2 instances, which leverage the ENA Network driver (<a href="https://github.com/amzn/amzn-drivers/blob/master/kernel/linux/ena/RELEASENOTES.md">ref</a>) as a standard traffic component. This Network driver works with AWS Elastic Network Interfaces (ENIs) and sets up receive and transmit queues for buffering packets. Our first symptom that something was wrong was identifying that whenever the ML training jobs failed with network connectivity issues, it correlated with a Network driver ‘<a href="https://github.com/amzn/amzn-drivers/blob/master/kernel/linux/ena/ENA_Linux_Best_Practices.rst">reset</a>’, as seen in our system logs.</p><pre>[] ena 0000:20:03.0 eth0: TX q 5 is paused for too long (threshold 5000000). Time<br>since last napi 6596000 usec. napi scheduled: 1<br>[] ena 0000:20:03.0 eth0: napi handler hasn&#39;t been called for a long time but is scheduled<br># .... Bunch of stats excluded....<br>[] ena 0000:20:03.0: ENA Large LLQ is disabled<br>[] ena 0000:20:03.0: Device reset completed successfully, Driver info: Elastic Net<br>work Adapter (ENA) v2.11.0g</pre><p>From the reference docs:</p><p><em>Q: What is [the] ENA device reset?</em></p><p><em>A: ENA device reset is a self healing mechanism that is triggered when the driver detects unexpected device behavior. Example of such behavior could be an unresponsive device, missing keep-alive events from the device, </em><strong><em>Tx completions timeouts</em></strong><em>, netdev timeout etc. The device reset is a rare event, lasts less than a millisecond and might incur loss of traffic during this time, which is expected to be recovered by the transport protocol in the instance kernel.</em></p><p><strong>Ok, so the driver saw Tx threads paused for an extended period of time (hardcoded to 5s in AWS ENA Kernel drivers), and caused the device to be reset, which could cause some packet drops.</strong> A typical reason for resets was documented as <a href="https://github.com/amzn/amzn-drivers/blob/master/kernel/linux/ena/ENA_Linux_Best_Practices.rst#cpu-starvation">CPU starvation</a>, i.e, when the Network driver’s threads don’t get CPU time for several seconds. So perhaps something CPU intensive was starving out the Network driver threads?</p><h3>Symptom 2: CPU utilization</h3><p>Our next observation was that some of the machines where we saw network resets exhibited high system CPU usage and that correlated nicely with the CPU starvation theory in the ENA documentation. We speculated that our training jobs were leveraging inefficient memory allocators and that was resulting in High page faulting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EE9kxYskRdD-5KXyU2B0Hw.png" /><figcaption><em>Figure 2: Page faults per second on impacted machines</em></figcaption></figure><p>We did what many reasonable people would do:</p><ul><li>We tried using Huge pages (by turning on <a href="https://docs.kernel.org/admin-guide/mm/transhuge.html">TransparentHugePages</a>) to reduce page faulting.</li><li>We experimented with more efficient memory allocators like <a href="https://jemalloc.net/">jemalloc</a></li><li>We tried to give the training jobs their own CPU cores by providing them CPU affinity via <a href="https://man7.org/linux/man-pages/man1/taskset.1.html">taskset</a>.</li><li>Out of desperation, we played with interrupt pinning for ENA drivers by steering network interrupts to other cores.</li></ul><p>Nothing worked. While we saw some drops in overall CPU utilization and page faulting from the memory allocators and huge pages settings, the network resets continued. They sometimes happened very early in a training job run and sometimes several hours into their execution. Across 100s of training job runs, it was hard to predict when exactly we’d see a network reset, if at all.</p><p><strong>One mitigation <em>did</em> work, albeit briefly and it’s everyone’s favourite <em>IT crowd</em> advice: Yes, we turned it off and on again. </strong>When we rebooted machines with high amounts of resets, they were able to support running ML jobs just fine.. that is until they weren’t. We clocked it at approximately one week of uptime, after which the network resets returned on the rebooted machines.</p><h3>Symptom 3: Availability zone differences</h3><p>To further understand the problem, the ML platform team started emitting metrics whenever an ENA reset was observed. Once the metrics were available, the team noticed something odd — the network resets were happening on machines in one AWS Availability zone only and all their jobs with identical parameters were running just great on other zones.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*etrE8z45SnNTa3RY55HQxQ.png" /><figcaption><em>Figure 3: Network resets per Availability Zone</em></figcaption></figure><p>The PinCompute team runs zonal clusters (one Kubernetes cluster per Availability zone) but when the team looked at our cluster configurations across different zones, they seemed identical. They were running the same version of Kubernetes and the same system image. So, did we get a bad hardware batch!? We reached out to our excellent AWS support team and after several engagements, were convinced that the issue was definitely not on the AWS side. Their analysis was clear: there was something on our machines in the us-east-1a zone, which was heavily using the CPU and causing the network threads starvation. So why would one availability zone’s machines only exhibit this network reset behaviour?</p><h3>Profiling attempts: perf and mpstat</h3><p>We decided it was time to stop with high level metrics and start profiling what was actually using the high amounts of CPU. Performance engineers know all about <a href="https://www.brendangregg.com/perf.html">perf</a> and its versatility. perf is a Linux profiler that can provide insights into ‘hot’ code paths and a call stack indicating CPU time spent by a particular process on a machine. Initially, our rudimentary snapshots of perf revealed the same suspected actors: Page faulting and some heavy computation from our ML jobs. However, this didn’t indicate CPU starvation all on its own.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lBgwqzlNnaBME__PO3Mdkw.png" /><figcaption><em>Figure 4: perf snapshot on an impacted machine</em></figcaption></figure><p>We realized that for CPU starvation to happen, it may take as little as one CPU core to be heavily utilized and block an unlucky network thread that was scheduled onto that core. Moreover, we realized that our GPU machines had 96 vCPU cores, which meant that an overall perf view told us very little about what was happening in each individual core.</p><p>To address this, we used <a href="https://linux.die.net/man/1/mpstat">mpstat</a> to get an overview of per core utilization on a per-second basis for an hour to identify if specific cores were using up large amounts of CPU. <strong>In our offline analysis, we found that sometimes, a single CPU core (in the following screenshot, CPU 39) was often using 100% of its system CPU for multiple seconds! </strong>This also correlated with when a network reset happened. We were finally closing in on the root cause!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rWXuQ_YVtG-e9WSmrrqHzA.png" /><figcaption><em>Figure 5: 100% System CPU utilization on a single core (Core 39) when profiled per second.</em></figcaption></figure><p>Given these network resets were happening at unpredictable times and we lacked perf runs from the times of the reset, we were still missing one key detail: what process was using up the CPU for this extended period of time?</p><h3>Temporal profiling: Time is an important factor</h3><p>We realized that if there was a sporadic process (think something in your crontab or some kind of periodic sync loop in a process) that was causing high CPU utilization at specific times on the machine, then a random perf sample wouldn’t tell us about that. We needed a tool like <a href="https://github.com/intel/gprofiler">gProfiler</a> to be running for an extended period of time and then ‘time travel’ to a specific point in time to look at what was happening on the CPU cores at that time. Unfortunately, at the time of this incident, we didn’t have gProfiler running everywhere within our fleet, but the principles were sound! Thanks to some creative setup from our ML platform team, we created the following experimental setup:</p><p>1. Reserved a small number of machines (via Kubernetes <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/">taints</a>) for analysis</p><p>2. Kicked off a series of training jobs in parallel on these machines. For simplicity, we repurposed our in-house Hyper-parameter tuning to orchestrate identical model training across reserved machines, allowing each training run’s resource footprint to remain fairly constant.</p><p>3. Kicked off a script that ran perf in 2 minute increments with profiles and CPU stacks data saved to disk. The script looked a bit like this and ran on all of our reserved machines as a system process.</p><pre># Bash program to generate CPU stacks snapshots on a machine. <br><br># Run perf record for 2 minutes at a time, since each perf data file can become very large for longer periods. Record the start time in the filename for &#39;time traveling&#39; later! Running this 360 times covers roughly a 12 hour period of profiles<br>$ for i in {1..360} <br>  do <br>    sudo perf record -F 97 -g -a -o perf-$(hostname)-$(date +&quot;%Y%m%d-%H-%M-%S&quot;)-120s.data -- sleep 120  <br>  done<br><br># Generate perf stacks<br>$ for datafile in `ls perf-*` <br>  do <br>    perf script --header -i $datafile &gt; $datafile.stacks<br>  done</pre><p>4. We ran the data collection overnight (~12 hours) and waited for a reset to be triggered. Since our ML training jobs typically ran for 8–12 hours, we were confident that we would observe a reset over this period across at least a subset of the training jobs.</p><p>Sure enough, when we came to analyze the data the next day, we found that network driver resets had been triggered along with Job failures. Unlike before, we now had perf data to examine from the time of the reset! We fetched the perf results for the 2 minute time window around the reset event and visualized it with the excellent <a href="https://github.com/Netflix/flamescope">Flamescope</a> tool, courtesy our friends at Netflix. Flamescope allows us to view a 2 minute CPU stack with a time travel view, allowing us to zoom into a subset of the time window and observe what was happening on the CPU <em>at that time. </em>From the ENA reset logs, we found that the reset had happened about 70 seconds into this profile, so we zoomed in to a 5 second region from the high-level view around the reset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KIoimNbGJtFaIa6INS5uSg.png" /><figcaption><em>Figure 6: Temporal high-level view of CPU utilization from flamescope. X-axis is time from 0–120 seconds for the 2 minute snapshot</em></figcaption></figure><p>Our first observation was that the kubelet, our lightweight Kubernetes agent, was occupying ~6.5% of total CPU usage a few seconds before an ENA reset. This was alarming and interesting because the rest of the time, the Kubelet barely broke 1% of CPU usage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VgPVLlK-5kpVBNYUewOg6w.png" /><figcaption><em>Figure 7: Profile of the CPU just before ENA resets. Notice the high kubelet utilization.</em></figcaption></figure><p>We zoomed in a bit deeper and found that the kubelet was spending a lot of time on a system call: <em>mem_cgroup_nr_lru_pages</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1-3NSvIcBCLgT_VAqmTcZQ.png" /><figcaption><em>Figure 8: Zoomed in profile of the CPU stacks for the kubelet process</em></figcaption></figure><p>We now had a suspect: something was causing the Kubelet to iterate over all the <a href="https://docs.kernel.org/admin-guide/cgroup-v1/memcg_test.html">memory cgroups</a> on the host and spending significant time on the CPU. At the same time when we were researching this, we came across this <a href="https://blogs.oracle.com/linux/zombie-memcg-issues">excellent post</a> on the Oracle blog describing the problem of <em>zombie memory cgroups. </em>Could we be running into this problem? Fortunately, that blog post guided us perfectly and we saw the following on a network driver resetting machine:</p><pre># Kernel tracked cgroups (including zombies)<br><br>$ cat /proc/cgroups | grep memory | awk &#39;{print $3}&#39;<br>68680<br><br># Actual cgroups<br><br>$ find /sys/fs/cgroup/memory/ -type d  | wc -l<br>240</pre><p>Yup, we definitely had zombies! Nearly 70,000 memory cgroups tracked in the Kernel but only 240 actually in use. Iterating over that long list in the system call was likely what was causing the CPU utilization spikes on a single core and if a network thread landed on that core at just the right time, it could become starved! But what was causing the high build up of memcgs?</p><h3>Beware of system defaults</h3><p>Our theory at this point was that the build up memcgs was from some crashlooping container, which kept re-creating cgroups and leaking memcgs. We didn’t see any such container created by Kubernetes but spotted a container that was always only a few seconds old when we queried the docker API:</p><pre>$ docker ps -a<br>CONTAINER ID   IMAGE                                                                                                                       COMMAND                  CREATED          STATUS                             PORTS     NAMES<br>c6fdfc760921   amazon/amazon-ecs-agent:latest                                                                                              &quot;/agent&quot;                 11 seconds ago   Up 10 seconds (health: starting)             ecs-agent</pre><p>Why was the Amazon ECS Agent running (and repeatedly crashing!) in our <em>Kubernetes</em> nodes? This was certainly unintentional given <a href="https://aws.amazon.com/ecs/">ECS</a> is an alternative container orchestration platform that we weren’t using. It turns out that for our GPU instances, we were leveraging the <a href="https://docs.aws.amazon.com/dlami/">AWS Deep Learning AMI</a> (Ubuntu 20.04) as a base machine image and it set up ecs-agent as a default systemd unit. <strong>As part of the machine’s bootstrap process, it also started the ECS agent, which over several days of crashing accumulated a massive amount of memory cgroups.</strong> The ECS Agent was correctly crashing since we did not give our machines permissions to join an ECS cluster and so it was natural that the container failed to start up. This also explained why rebooting the machines gave us temporary relief because rebooting reset the memcg counts!</p><p><strong>We fixed the issue by simply turning off the ECS agent systemd unit in our base images and rebooting all our machines to purge the zombie memcgs</strong>. After this, we noticed that memory cgroups remained stable and most importantly, Ray Training jobs were running with their expected high success rate again. The problem of ENA resets and the zombies in our machines was fully resolved and our ML training teams could go back to building awesome new models to serve Pinterest customers!</p><h3>Hold on! What about the availability zones disparity?</h3><p>Oh.. right. Well, erm, we messed up a little. See, when we said that the two node configurations were identical across the two clusters, that was only <em>mostly </em>true. For our Kubernetes cluster in the unaffected availability zone, we had an independent bug where we delivered the <strong>same Kubernetes binary</strong> via two different URLs to the two clusters. Long story short, the difference in URLs caused a last step that emitted a metric to fail and caused the node bootstrap script to get marked as failed. This prevented the ECS agent from starting up because <strong><em>its</em> systemd unit depended on the bootstrap script to successfully complete,</strong> which in turn allowed the nodes to remain ‘healthy’, at least from the perspective of not accumulating memcgs! The Kubernetes team was aware of this different URL issue and was independently working on fixing that as well, which in turn would have brought the network reset issue to the unaffected Availability zone as well.</p><h3>Key Takeaways</h3><ul><li>Introducing fleet wise metrics to track transient issues on the Platform is helpful to identify failure patterns. In this case, it helps us understand that the issue was correlated to AZ/Cluster setup, further leading us to isolate and consistently reproduce the problem.</li><li>Create reproducible, closed environments for iterative debugging. In our case, the partnership between the PinCompute and ML Platform teams to set up debugging experiments was critical to quickly identifying the root cause of the issue.</li><li>Invest in profiling tools and especially temporal profiling tools! They’re great and will save you hours and hours when working on hard-to-debug performance problems. At Pinterest, we’re developing and rolling out <a href="https://github.com/intel/gprofiler">gProfiler</a> in close collaboration with Intel for debugging situations like this.</li><li>Be aware of what processes are running on your base OS images. Sometimes, the defaults aren’t necessarily the right ones for your environment. Invest in profiling the success rate of your systemd units and watch out for the impact of regular failures.</li><li>When looking at differences between two environments that look the same but act differently, look closer.. You’re probably missing some piece of configuration that is causing the two paths to diverge. Better yet, invest in good automated tooling to ensure your environments are truly identical.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea4722e552eb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/finding-zombies-in-our-systems-a-real-world-story-of-cpu-bottlenecks-ea4722e552eb">Finding zombies in our systems: A real-world story of CPU bottlenecks</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 Recommendation Systems with Request-Level Deduplication]]></title>
            <link>https://medium.com/pinterest-engineering/scaling-recommendation-systems-with-request-level-deduplication-93bd514142d9?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/93bd514142d9</guid>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Mon, 13 Apr 2026 19:01:01 GMT</pubDate>
            <atom:updated>2026-04-13T19:01:01.524Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Authors:</strong> Matt Lawhon | Sr. Machine Learning Engineer; Filip Ryzner | Machine Learning Engineer II; Kousik Rajesh | Machine Learning Engineer II; Chen Yang | Sr. Staff Machine Learning Engineer; Saurabh Vishwas Joshi | Principal Engineer</p><p>At Pinterest, scaling our recommendation models delivers outsized impact on the quality of the content we serve to users. Our <a href="https://arxiv.org/abs/2507.12704">Foundation Model</a> (oral spotlight, ACM RecSys 2025), for example, achieved a 100x increase in transformer dense parameter counts and a 10x increase in model dimension; translating directly into meaningful quality improvements across multiple recommendation surfaces.¹</p><p>But a 100x scaleup creates massive infrastructure pressure. Storage, training, and serving costs all threaten to grow proportionally unless you’re deliberate about efficiency. The single highest-impact technique we’ve deployed to hold costs in check across all three dimensions is <strong>request-level deduplication:</strong> a family of techniques that ensures we process and store request-level data once, not once per item.</p><p>In this post, we’ll walk through what request-level deduplication is, why it matters so much for modern recommendation systems, and how we applied it across the full ML lifecycle , from storage compression to training correctness and speedups to serving throughput gains.</p><h3>Background</h3><p>A <em>request</em> is triggered when a user opens their feed, kicking off the recommendation funnel:</p><ul><li><strong>Retrieval</strong>: Aggregate user and request information into one or multiple embeddings, then fetch a large set of potentially relevant items from the entire corpus using techniques like nearest neighbor search.</li><li><strong>Ranking</strong>: Aggregate user, request, and item information to make predictions about relevance or engagement. Typically there are early-stage ranking models (which need cheap per-item inference since they score many items) and late-stage ranking models (which can afford more expensive per-item inference since fewer items are ranked).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t3rKtnFd9I7yiyfjslqg4w.png" /></figure><p>The same user data flows through every stage of this funnel, and within each stage, it’s duplicated across every item scored. Request-level deduplication refers to the category of techniques that eliminate this redundancy when storing, moving, or transforming this data.</p><p>The impact can be extremely high because:</p><ul><li><strong>Request-level data is massive.</strong> It largely consists of user sequences, approximately 16K tokens encoding all actions a user has taken on the platform. These sequences power sequential user understanding components like the <a href="https://arxiv.org/abs/2507.12704">Pinterest Foundation Model</a> and <a href="https://arxiv.org/abs/2506.02267">TransAct</a>. Each sequence is duplicated identically for every candidate item scored, hundreds to thousands of copies per request.</li></ul><p><strong>Processing this data is expensive.</strong> The computation associated with user tower models in retrieval and user sequence understanding components in ranking represents a significant proportion of total recommendation system compute.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/954/1*kWBPDdEOxaoI-VWGfpVfdw.png" /></figure><h3>Storage</h3><p>One of the key ways deduplication pays off is at the storage level. A row in a training dataset typically consists of [request/user, content item, engagement label], and we can have hundreds or thousands of content/engagement labels associated with a single request. Without deduplication, the same massive user sequence is stored redundantly for every single content interaction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vmof2oFg1mMtESr2y-UIsg.png" /></figure><p>By leveraging <a href="https://iceberg.apache.org/">Apache Iceberg</a> with user ID and request ID based sorting (<a href="https://medium.com/pinterest-engineering/how-pinterest-accelerates-ml-feature-iterations-via-effective-backfill-d67ea125519c">How Pinterest Accelerates ML Feature Iterations via Effective Backfill</a>, <a href="https://medium.com/pinterest-engineering/scaling-pinterest-ml-infrastructure-with-ray-from-training-to-end-to-end-ml-pipelines-4038b9e837a0">Scaling Pinterest ML Infrastructure with Ray</a>), we achieve 10–50x storage compression on user-heavy feature columns.² When rows sharing the same request are physically co-located, columnar compression algorithms handle the deduplication automatically.</p><p>Beyond raw storage savings, request-sorted data enables improved dataset tooling:</p><ul><li><strong>Bucket joins</strong>: Matching keys are co-located, eliminating expensive shuffle operations.</li><li><strong>Efficient backfills</strong>: We can update only affected user segments rather than reprocessing entire datasets.</li><li><strong>Incremental feature engineering</strong>: Adding new request-level features becomes a localized operation: we can append new columns to existing row groups without duplicating the entire dataset.</li></ul><p><strong>Stratified sampling</strong>: Request-sorted data enables user-level sampling, ensuring training datasets maintain proper diversity without over-representing highly active Pinners.</p><h3>Training</h3><h4>Addressing Independent and Identically Distributed (IID) Disruption</h4><p>Early experiments with request-sorted data revealed 1–2% regressions on key offline evaluation metrics in our ranking models.² The root cause was the disruption of the IID assumption.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3uAJ__0WgVx6wVEcLNJfUQ.png" /></figure><p>With IID sampling, each batch contains engagements spread across many users, yielding stable and representative statistics. With request-sorted data, batches become concentrated around fewer users, causing batch-level statistics to fluctuate dramatically based on individual user behavior. Each gradient update is computed from a less representative slice of the data: the model sees a noisier, more biased view of the training distribution, which slows convergence and degrades final quality.</p><p>The specific vulnerability lies in Batch Normalization (BatchNorm), which normalizes intermediate values by computing mean and variance <em>across the batch</em>. Standard BatchNorm computes these statistics independently on each device’s local batch. When batches are request-sorted and highly correlated, a batch dominated by a single power user will have dramatically different statistics than one with a casual browser.</p><h3>Fix: Synchronized Batch Normalization (SyncBatchNorm)</h3><p>SyncBatchNorm aggregates statistics across all devices before normalization. This effectively increases the “statistical batch size” used for computing means and variances, even though each device still processes its local request-sorted batch. The result is that normalization statistics are computed over a much more diverse set of users and requests, restoring the representative statistics that standard BatchNorm enjoyed with IID data.</p><p>In practice, this simple one-line change fully recovered the performance gap. The communication overhead of synchronizing statistics across devices was negligible compared to the training speedups gained from deduplicated computation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8QD9F1V0lpKGjMyW7nQGdw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-904a8xg-X9sL4XTl_e6hw.png" /></figure><p>With IID sampling, the probability that a randomly sampled in-batch negative is actually a positive for the anchor user is negligible: users engage with a tiny fraction of the total item corpus. With request-sorted data, however, batches are concentrated around fewer users, and each user may have dozens or hundreds of engagements grouped together. Many in-batch “negatives” are actually items the user engaged with, they’re false negatives. The false negative rate jumps from ~0% with IID sampling to as high as ~30% with request-sorted data, depending on the number of unique users per batch.²</p><p>Training the model to push apart items the user <em>actually</em> engaged with actively degrades retrieval quality.</p><h3>Fix: User-Level Masking</h3><p>To address this, we extended our existing identity masking to also exclude negatives that belong to the same user as the anchor. The standard InfoNCE loss with logit correction:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dFqGHbIUA-0iEUodFdW8pw.png" /></figure><p>becomes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UdjsphIzbevwYdewpg5COw.png" /></figure><p>where:</p><ul><li><em>s(·,·)</em> is the similarity function (e.g., dot product) between user and item embeddings</li><li><em>x_i</em> is the user embedding for the anchor engagement <em>i</em></li><li><em>y_i</em> is the positive (target) item for engagement <em>i</em></li><li><em>y_k</em> represents candidate negative items from batch <em>B</em></li><li><em>x_k</em> is the user associated with candidate <em>k</em></li><li><em>p_y</em> values are streaming frequency estimates (<a href="https://research.google/pubs/pub48840/">Yi et al., 2019</a>) used for logit correction</li><li><strong><em>x_k ≠ x_i</em></strong> is the new constraint: only use engagements from <em>other</em> users as negatives</li></ul><p>This simple masking change allowed us to successfully adopt request-sorted data for retrieval model training while preserving model quality.</p><h3>Manifesting Training Speedups</h3><p>The previous sections focused on correctness, ensuring model quality is preserved when switching to request-sorted data. Here we discuss how to actually realize the compute and memory savings that deduplication enables.</p><h4>Data Loading</h4><p>Our data loading infrastructure, shared across ranking and retrieval models, is designed to maintain deduplication as long as possible in the pipeline. All preprocessing and feature transformations operate on deduplicated request-level data. We only reduplicate (expand) at the very end, on GPU or directly in the model’s forward pass. This minimizes CPU-to-GPU transfer costs and memory allocation overhead.</p><h4>Retrieval Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-9CCQgRrDZj8PPdN4bsL_Q.png" /></figure><p>Achieving request-level compute deduplication in retrieval models is straightforward thanks to the two-tower architecture. Since the user tower has no item dependencies by definition, we rewrite the forward pass to run the user tower on the deduplicated batch of <em>R</em> unique requests rather than the full batch of <em>B</em> user-item pairs. The item tower continues to operate on the full batch. Gradients for the user tower are computed at the deduplicated level and appropriately accumulated.</p><p>Though conceptually simple, the savings compound in practice, memory allocation, I/O, and compute all benefit, particularly for large user sequence models where the user tower dominates training cost.</p><h3>Ranking Models: Deduplicated Cross-Attention Transformer (DCAT)</h3><p>Ranking models present a greater challenge because transformer architectures used for user understanding typically have item dependencies: each candidate item attends to the user history, coupling request-level and item-level computation.</p><p>To address this, we developed DCAT, described in detail in the <a href="https://arxiv.org/abs/2507.12704">Pinterest Foundation Model paper</a>. The key insight is to separate the transformer into two components:</p><ol><li><strong>Context</strong>: Apply the transformer to the user’s historical action sequence once per deduplicated request. The keys and values (KV) from each layer are cached.</li><li><strong>Crossing</strong>: Each candidate item performs cross-attention with the cached user history KV, reusing the deduplicated context computation.</li></ol><p>This optimization, implemented with custom <a href="https://triton-lang.org/">Triton</a> kernels for both training and serving, achieved significant throughput gains over standard self-attention with <a href="https://arxiv.org/abs/2205.14135">FlashAttention</a>.</p><h3>Training Impact</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8lOk3mOBJrqkS0Si2BuQyQ.png" /></figure><p>Taken together, request-level deduplication delivered a <strong>4x end-to-end training speedup for retrieval</strong> and a <strong>~2.8x speedup for ranking</strong> (40% from deduplicated data loading compounded with a 2x gain from DCAT cross-attention).²</p><h3>Serving</h3><p>For retrieval, serving has always been correctly deduplicated by design: we embed the user once and search against the item index. No changes were needed.</p><p>For ranking, the DCAT architecture provides the same deduplication benefit at serving time as it does during training. The context transformer processes the user’s action sequence once per request, the key-value (KV) cache stores the intermediate representations, and each candidate item cross-attends to this cached context. This avoids redundantly recomputing the full user sequence for every item scored.</p><p>The result is a <strong>7x increase in ranking serving throughput</strong>.² This is what made it possible to deploy a 100x larger model without proportional serving cost increases, absorbing the full Foundation Model scaleup while holding infrastructure budgets in check.</p><h3>Conclusion</h3><p>Request-level deduplication delivered impact across every layer of our ML lifecycle:</p><ul><li><strong>Storage</strong>: 10–50x compression on user-heavy feature columns via Iceberg and request sorting²</li><li><strong>Training</strong>: 4x retrieval speedup and 2.8x ranking speedup from deduplicated data loading and DCAT²</li><li><strong>Serving</strong>: 7x throughput increase via DCAT and custom Triton kernels²</li></ul><p>Three lessons stand out:</p><ol><li><strong>Request-level deduplication is a cross-cutting technique.</strong> It improves storage, training, and serving simultaneously because the same fundamental redundancy exists at every layer.</li><li><strong>Simple fixes unlock big wins.</strong> SyncBatchNorm and user-level masking are minimal code changes with outsized impact. The hardest part was identifying the problems; the solutions were straightforward.</li><li><strong>Impact compounds across the stack.</strong> Storage compression enables faster data pipelines, training speedups accelerate experimentation velocity, and serving throughput reduces infrastructure cost, freeing capacity for the next round of model scaling.</li></ol><p><em>¹ </em><a href="https://arxiv.org/abs/2507.12704"><em>Pin Foundation Model</em></a><em>, ACM RecSys 2025.</em> <em>² Pinterest Internal Data, Global, 2025.</em></p><h3>Acknowledgements</h3><p>This work reflects joint efforts across multiple teams at Pinterest. We’d like to thank: Devin Kreuzer, Piyush Maheshwari, Hanlin Lu, Xue Xia, Abhinav Naikawadi, Yuming Chen, and Aditya Mantha (Personalization); Kousik Rajesh, Xiangyi Chen, Zelun Wang, Hanyu Li, Pong Eksombatchai, Jaewon Yang, Yi-Ping Hsu, and Hongtao Lin (Applied Sciences); Raymond Lee, Sheng Huang, Neha Upadhyay, Nazanin Farahpour, Henry Feng, Alekhya Pyla, Rubin Fergerson, and Shengtong Zhang (ML Platform); Shivin Thukral, Joseph Bongo, Zach Barnes, and Yang Cao (Search); and Anya Trivedi, Akshay Iyer, and Rui Liu (Notifications).</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=93bd514142d9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/scaling-recommendation-systems-with-request-level-deduplication-93bd514142d9">Scaling Recommendation Systems with Request-Level Deduplication</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[Performance for Everyone]]></title>
            <link>https://medium.com/pinterest-engineering/performance-for-everyone-21a560260d08?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/21a560260d08</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[performance-metrics]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[user-experience]]></category>
            <category><![CDATA[performance]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Wed, 08 Apr 2026 16:01:01 GMT</pubDate>
            <atom:updated>2026-04-08T16:01:01.814Z</atom:updated>
            <content:encoded><![CDATA[<p>Author: Lin Wang (Android Performance Engineer)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aAqbT-AdcudKcE8RPb8w4A.png" /></figure><h4><strong>Default Feature</strong></h4><p>For mobile apps, performance is considered as the “default feature”, which means apps are expected to run fast and be responsive. It’s just as if we expect a watch to show the time. With no exceptions at Pinterest, we measure, protect and improve performance for all of our key user experiences’ surfaces, such as “Home Feed” and “Search Result Feed”.</p><h4><strong>Hard to Measure</strong></h4><p>Among all the performance metrics, the <strong>user perceived latency</strong> is a crucial one. It measures how much time the user spends since they perform an action until they see the content. This is also called “<strong>Visually Complete</strong>”.</p><p><strong>Visually Complete</strong> can be very different from app to app or even from surface to surface within one app. On Pinterest’s “Video Pin Closeup” surface, <strong>Visually Complete</strong> means the full-screen video starts playing; on our “Home Feed” surface, <strong>Visually Complete</strong> is defined as all the images rendered and videos playing; on our “Search Auto Complete Page”, <strong>Visually Complete </strong>refers to the search autocompleted suggestions’s text rendered along with the avatar images.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CqFgL-xHHzwp0sJIPCkgkQ.png" /></figure><p>Given this dynamic nature of <strong>Visually Complete</strong>, engineers had to create customized measurement logic for each surface and that takes a lot of engineering effort and maintenance cost. This ends up as a major boundary for general product engineers to work on performance, especially on newly created surfaces. On average, it takes <strong>two engineer-weeks</strong> to implement a User Perceived Latency metric on the Android Client and wire it up to all the toolsets for production usage.</p><h4><strong>All-In-One Solution</strong></h4><p>Over the years, the performance team at Pinterest has been thinking about how to offer performance measures with the lowest cost to product engineers. Therefore, more product engineers can more easily have access to their feature’s user perceived latency information and work on performance.</p><p>Until recently, it seems we have found an answer to this. In a nut shell, we built the <strong>Visually Complete</strong> logic into the base UI class (e.g. <strong>BaseSurface</strong>). Therefore, the <strong>Perceived Latency </strong>of any UI surface (existing or new) will be automatically measured as long as the feature is built on top of this base UI class.</p><h4><strong>Walk the View Tree</strong></h4><p>First we define a few common media view interfaces: <strong>PerfImageView</strong>, <strong>PerfTextView</strong>, <strong>PerfVideoView</strong>. Each of them contains a few methods to report their rendering status: <strong>isDrawn()</strong>, <strong>isVideoLoadStarted()</strong>, <strong>x(),</strong> <strong>y()</strong>, <strong>height()</strong>, <strong>width(),</strong> etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cziK0nCGc-N01lnwxmKfWw.png" /></figure><p>At the <strong>BaseSurface</strong> level, given that we should have access to the root android ViewGroup (e.g. <strong>RootView</strong>). We could just iterate through the view tree starting from the <strong>RootView </strong>by visiting all the views on this tree. We will focus on those visible views and judge if all the <strong>PerfImageView</strong>, <strong>PerfTextView</strong> and <strong>PerfVideoView</strong> instances are all drawn or started if it’s a video.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gMTthN7j-Afym3txQUx-8g.png" /></figure><h4><strong>In Production</strong></h4><p>Since the release of this system on Android, it constantly visualizes the User Perceived Latency on over <strong>60 surfaces</strong> at any given time. It is well received by many product teams and started to protect and improve their surface’s performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aLn9Q_fxY3Oc-acZg2MwTA.png" /></figure><h4><strong>Interesting Cases</strong></h4><ul><li>Since all surfaces are measured by the same standard, we can compare multiple surfaces’ performance fairly.</li><li>For some features with short shelf time (e.g. a Christmas landing page), we previously weren’t able to code their latency metrics in time, but now those latency metrics will be ready since the surface is built.</li></ul><h4><strong>Conclusion</strong></h4><p>Once the performance metrics are offered to product engineers for free, it makes Pinterest’s performance more visible and encourages everyone to protect and optimize the User Perceived Latency on their surfaces.</p><p>Following the success on Android, we have also extended the same concept to iOS and web platforms.</p><h4><strong>Acknowledgements</strong></h4><p>Special thanks: Arun K</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=21a560260d08" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/performance-for-everyone-21a560260d08">Performance for Everyone</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[Evolution of Multi-Objective Optimization at Pinterest Home feed]]></title>
            <link>https://medium.com/pinterest-engineering/evolution-of-multi-objective-optimization-at-pinterest-home-feed-06657e33cd10?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/06657e33cd10</guid>
            <category><![CDATA[results-diversification]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[slate-optimization]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[pinterest]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Tue, 07 Apr 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-04-21T18:11:22.795Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Homefeed: </strong>Jiacong He, Dafang He, Jie Cheng (former), Andreanne Lemay, Mostafa Keikha, Rahul Goutam, Dhruvil Deven Badani, Dylan Wang<br><strong>Content Quality:</strong> Jianing Sun, Qinglong Zeng<br><strong>ML Serving: </strong>Li Tang</p><h3>Introduction</h3><p>In feed recommendation, we recommend a list of items for the user to consume. It’s typically handled separately from the ranking model where we give probability predictions of user-item pairs.</p><p>Pinterest’s feed recommendation follows a cascaded system design with retrieval [1][2], pre-ranking [3], ranking [4][5], and re-ranking. While most of these prior works focus on optimizing immediate actions for each candidate Pin, this work will primarily focus on how we build the final layer of the recommendation funnel for multi-objective optimization. This is a critical part of our recommendation system as it helps us balance short-term and long-term engagement, drive new use case adoption, and satisfy various business requirements. Throughout the years, we have made substantial improvements on this layer through both algorithmic and infrastructure upgrades. In this tech blog post, we will share our experiences, learnings and improvements we’ve made over the years on this critical layer.</p><h3>Overall System Design</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nn5FGuO-CFUwCDLlt5swNA.png" /><figcaption>Figure 1. Cascaded Design of Pinterest Funnel.</figcaption></figure><p>Figure 1 illustrates the cascaded funnel design of our feed recommendation system from retrieval to ranking to the multi-objective optimization component. While earlier stages mostly optimize for certain positive actions (e.g., saves) given an impression, the multi-objective optimization layer tackles a different problem: determining the best composition of a feed served to the user. This is critical as users tend to have lower intent when visiting Home Feed and their browsing behavior will be significantly impacted by what they see. For example, visually repetitive content is less engaging and is likely to reduce the user’s session length and the likelihood that a user will revisit Pinterest.</p><h3>Multi-Objective Optimization Design</h3><p>In this section, we describe the detailed design of our multi-objective optimization layer.</p><h4>Diversification</h4><p>Feed diversification is an important factor for continued user satisfaction. We empirically found that when removing the feed-level diversity component, users’ immediate actions (e.g., saves) increase on day 1 but quickly turn <em>negative</em> by the second week. This also comes with a reduced session time and other negative downstream effects which significantly reduces the user’s long-term satisfaction. It is important to note that when users engage with less diverse content, engagement signals will also be affected, reinforcing the system to generate less diverse content.</p><p>To achieve better short-term and long-term engagement, we applied a diversity-based re-ranking algorithm in our feed as the main part of the multi-objective optimization layer. It is also one of the most important parts of the multi-objective re-ranking system.</p><h4>V1: Determinantal Point Process (DPP)</h4><p>DPP is widely used in the industry for feed diversification [6][7]. In our first generation of feed diversification, we leveraged DPP as the main component.</p><p>Mathematically, DPP is parametrized by a kernel matrix Lₙₓₙ where the diagonal entry Lᵢᵢ measures the relevance/quality of the i-th item, and the off-diagonal entries Lᵢⱼ = Lⱼᵢ measure the similarity between item i and j. Practically, we use learned embedding such as GraphSAGE [8] and categorical taxonomy as a lever to determine item and item similarity. Thus, DPP’s kernel matrix can be generalized to L = f₀(Λ) g𝜓(S) f₀(Λᵀ) where Λ is the diagonal matrix whose diagonal entries are relevance scores of items, f₀(·) is a monotonic increasing element-wise transformation.</p><p>Our first version of the feed diversification algorithm was implemented in 2021 based on the DPP algorithm.</p><p>Since its launch, it has become one of the most impactful components in our system. As the system becomes increasingly responsive through more real-time signal adoption such as in TransACT[5], we have found out that user satisfaction improves when they have more diverse feed recommendations through DPP. We conducted an ablation study by removing the DPP component and found that the user’s time spent impression reduced by over 2% after the first week.</p><h4>V2: Sliding Spectrum Decomposition</h4><p>Sliding Spectrum Decomposition (SSD) [9] is a position‑adaptive diversification method used in the recommendation system that views a candidate feed as a mixture of latent “spectra” (topics/intents/styles). As we render the feed top‑down, SSD repeatedly decomposes the local similarity structure within a sliding window and rebalances exposure: under‑represented spectra are promoted while over‑represented spectra are softly penalized. This yields locally smooth yet globally balanced diversity, complementing slate‑global methods like DPP.</p><p>Mathematically, let X ∈ Rⁿˣᵈ be item embeddings and S ∈ Rⁿˣⁿ a symmetric similarity matrix built from learned representations (e.g., GraphSAGE). At position <em>t</em> with window size <em>w</em>, restrict S to the window S^(ᵗ) and compute a top-K spectral decomposition S^(ᵗ) ≈ U^(ᵗ) Λ^(ᵗ) U^(ᵗ)ᵀ. Let r ∈ Rⁿ be base relevance scores. SSD tracks cumulative exposure Eₖ(𝑡) per local spectrum k and defines an adjusted utility: Uᵢ(𝑡) = f(rᵢ) − β ∑ₖ₌₁ᴷ wₖ(𝑡)·(uₖ^(ᵗ)[i])² where f(·) is a monotone transform of relevance, β controls diversity strength, and wₖ(𝑡) increases with exposure relative to current spectral mass (e.g., wₖ(𝑡) ∝ Eₖ(𝑡) / (ε + λₖ^(ᵗ)). The next item is <em>i</em>⁎ = argmaxᵢ(Uᵢ(𝑡)); exposures are updated and the window slides.</p><p>Compared to DPP, sliding spectrum decomposition has lower computational complexity given that it avoids Cholesky-style similarity matrix decompositions. The original paper introducing SSD algorithm (<a href="https://arxiv.org/pdf/2107.05204">link</a>) gave a comprehensive comparison between different variations of DPP algorithms vs SSD algorithms:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*czzIt1PoaySCQL5N0D7rpA.png" /><figcaption>Table 1: Comparisons of greedy inference complexity for SSD and DPP with dense item embeddings. In general, we have 𝑁 &gt; 𝑇 &gt; 𝑤 and 𝑑 &gt; 𝑤. [9]</figcaption></figure><p>Moreover, the implementation logic of sliding spectrum decomposition is built from standard linear-algebra blocks (windowed similarity, top-K eigen/SVD, weighted penalties, etc.) and can be implemented cleanly in PyTorch with straightforward operations. It avoids positive semi-definite enforcement, log-determinants, and fragile numerical issues common in DPP (e.g., jittered kernels, Cholesky failures), enabling a straightforward “PyTorch-style” model approach with vectorized scoring and lower serving latency.</p><p>In early 2025, we launched the SSD algorithm, leveraging PyTorch for its diversification logic. This was executed on our company-wide model serving clusters. The SSD algorithm’s simplicity allowed us to incorporate more features for evaluating pairwise Pin similarities, ultimately leading to improved balance between engagement and diversification.</p><h4>Unified Soft-Spacing Framework</h4><p>With SSD it further enabled us to incorporate quality goals when evaluating pairwise pin similarities in the backward window. For content less aligned with our quality standards, we added a quality penalty score on top of the SSD objective for which we call it “soft spacing”, as it allowed us to avoid having these content clustered together while also balancing with engagement and diversification.</p><p>We define the soft spacing penalty: qᵢ(t) = 𝟙[cᵢ ∈ R] ∑<em>{d=1}^w (1/d) 𝟙[c</em>{t−d} ∈ R]. It’s applied when item <em>i</em> belongs to the sensitive set <em>R</em> and nearby previously placed items in the backward window also belong to <em>R</em>, with each prior item inversely weighted by distance. We then subtracted the soft spacing penalty term to the adjusted utility Uᵢ(t) with a coefficient λ to balance with other objectives.</p><p>This is an important next step for improving content quality on Pinterest and protecting users from content that warrants additional caution, where in the past we usually rely on strong enforcement like filtering which sometimes leads to less satisfying user experience if there is no backfill. In mid 2025 we launched the soft spacing penalty on content with elevated quality risk, to restrict its distribution and ensure the utmost quality standards at Pinterest. In late 2025 we further abstracted the logic via building an easy to use, config-based framework to make it more extendable to meet and adapt to quality needs.</p><h4>System Infrastructure Evolution</h4><p>At the launch of DPP, the main multi-objective optimization (blending) layer is composed of a sequence of “nodes.” Several Lightweight Reranking nodes first perform low-latency reordering to optimize for short-term engagement and coarse diversity. Candidate pins are then passed to the DPP node, where the more time-intensive DPP algorithm is applied. Before the system outputs the final recommendation list, additional heuristic reordering logic is still needed, such as the spacing strategies mentioned earlier. This chain of nodes is embedded within the Home Feed recommendation backend system. While this setup is relatively robust because it can directly leverage existing backend dependencies, it makes iteration on blending-layer logic challenging due to limited flexibility for local testing and the difficulty of experimenting with new features.</p><p>With the introduction of SSD, a significant portion of the blending layer’s logic, including much of the diversification logic, has been migrated to PyTorch and is now hosted within the company’s model serving cluster. Our ongoing efforts aim to transfer more heuristic logic from the blending layer to the model server, thereby simplifying chain execution within the blending layer.</p><p>Evolution of blending layer is exemplified by the graph below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-exW-8kiyf2wFzN98vxSeg.png" /><figcaption>Figure 2. Homefeed Blender System Infrastructure Evolution.</figcaption></figure><h4>Evolution of Diversity Signals</h4><p>With DPP, our feed diversification stack relied primarily on categorical signals (taxonomy labels such as home decor, fashion, cooking, etc.) and on GraphSage as the primary mechanism for defining similarity between Pins.</p><p>In early 2025, we migrated our diversification process to a CPU-served SSD algorithm implemented in PyTorch. This made it easier to incorporate richer embedding representations when computing pairwise Pin similarity. SSD’s lower serving latency, relative to DPP, allows us to use a broader set of signals. Specifically, SSD uses the following embeddings to represent Pins and drive diversification:</p><p><strong>Visual embeddings</strong>: capture visual redundancy and style similarity.</p><p><strong>Text embeddings</strong>: capture overlap in titles and descriptions.</p><p><strong>Graph embeddings</strong> (GraphSage): capture relatedness in the Pin graph, including co-engagement patterns and neighborhood similarity.</p><p>In Q2 2025, we added soft-spacing capabilities to address a business need: reducing clustered content exposure without relying on brittle, one-size-fits-all hard-spacing rules. As part of this work, we incorporated content quality signals that identify content requiring additional caution, allowing SSD to demote a candidate when similar content has appeared within a preceding window.</p><p>In Q3 2025, we upgraded SSD’s visual embedding to use PinCLIP image features [10]. PinCLIP provides a stronger multimodal visual representation, learned through image-text alignment with additional graph-aware objectives. Critically, this signal is also available in near real-time, which improves representation quality and, in turn, downstream similarity and diversification behavior, for recently ingested Pins.</p><p>More recently, in Q4 2025, we added a Semantic ID signal [11] to address a practical gap: while embeddings are excellent at capturing how close two Pins are, they do not always provide a stable, category-like notion of semantics that is useful for controlling diversity. Semantic IDs provide a hierarchical representation derived through coarse-to-fine discretization of content representations, enabling us to reason more explicitly about semantic overlap between items. In SSD, we discourage recommending too many Pins with high Semantic ID prefix overlap by applying a penalty term. This improves both perceived diversity and engagement by reducing repeated content clusters.</p><p>For future works, we are focusing on ensuring diversity across user specific interests and having a proper representation of the interests the user historically engaged with.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Hai8CwUmLUN1FV8Fet_bw.png" /><figcaption>Figure 3: Diversity component timeline</figcaption></figure><h4>On-going and Future Works</h4><p>Currently, we have various different on-going works to optimize the final layer. This includes two major workstreams: 1) a unified generative post-ranking model that optimizes the final slate generation in an end-to-end manner 2) reinforcement learning based value model.. We will share more details in later blog posts.</p><h4>Acknowledgement</h4><p>We would like to thank all of our collaborators across Pinterest. Ruimin Zhu, Yaron Greif, Ludek Cigler, Jason Madeano, Alekhya, Jaewon Yang, Xianxing Zhang</p><p><strong>Reference:<br></strong>[1] <a href="https://medium.com/pinterest-engineering/establishing-a-large-scale-learned-retrieval-system-at-pinterest-eb0eaf7b92c5">Establishing a Large Scale Learned Retrieval System at Pinterest</a><br>[2] <a href="https://medium.com/pinterest-engineering/advancements-in-embedding-based-retrieval-at-pinterest-homefeed-d7d7971a409e">Advancements in Embedding-Based Retrieval at Pinterest Homefeed</a><br>[3] <a href="https://medium.com/pinterest-engineering/pinterest-home-feed-unified-lightweight-scoring-a-two-tower-approach-b3143ac70b55">Pinterest Home Feed Unified Lightweight Scoring: A Two-tower Approach</a><br>[4]<a href="https://arxiv.org/abs/2209.08435"> Rethinking Personalized Ranking at Pinterest: An End-to-End Approach</a><br>[5] <a href="https://arxiv.org/abs/2306.00248">TransAct: Transformer-based Realtime User Action Model for Recommendation at Pinterest</a><br>[6]<a href="https://arxiv.org/abs/1207.6083"> Determinantal point processes for machine learning</a><br>[7] <a href="https://jgillenw.com/cikm2018.pdf">Practical Diversified Recommendations on YouTube with Determinantal Point Processes</a><br>[8]<a href="https://arxiv.org/abs/1706.02216"> Inductive Representation Learning on Large Graphs</a><br>[9] <a href="https://arxiv.org/abs/2107.05204">Sliding Spectrum Decomposition for Diversified Recommendation</a><br>[10]: <a href="https://arxiv.org/pdf/2603.03544">PinCLIP: Large-scale Foundational Multimodal Representation at Pinterest</a><br>[11] <a href="https://arxiv.org/pdf/2305.05065">Recommender Systems with Generative Retrieval</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=06657e33cd10" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/evolution-of-multi-objective-optimization-at-pinterest-home-feed-06657e33cd10">Evolution of Multi-Objective Optimization at 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>
        <item>
            <title><![CDATA[Building an MCP Ecosystem at Pinterest]]></title>
            <link>https://medium.com/pinterest-engineering/building-an-mcp-ecosystem-at-pinterest-d881eb4c16f1?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/d881eb4c16f1</guid>
            <category><![CDATA[engineering-culture]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Thu, 19 Mar 2026 16:01:01 GMT</pubDate>
            <atom:updated>2026-03-19T16:01:01.208Z</atom:updated>
            <content:encoded><![CDATA[<p>Tan Wang | Software Engineer, Agent Foundations</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NxS4wACf5xatHauDP_ExXQ.png" /></figure><p>Over the last year, Pinterest has gone from “MCP sounds interesting” to running a growing ecosystem of <strong>Model Context Protocol (MCP) servers</strong>, a <strong>central registry</strong>, and production integrations in our IDEs, internal chat surfaces, and AI agents. This post walks through what we’ve built so far, how we designed it, and where we’re taking MCP next.</p><h3>What Is MCP and Why Did We Care?</h3><p><a href="https://modelcontextprotocol.io/docs/getting-started/intro"><strong>Model Context Protocol (MCP)</strong></a> is an open-source standard that lets large language models talk to tools and data sources over a unified client-server protocol, instead of bespoke, one-off integrations for every model and every tool. At Pinterest, we’re using MCP as the substrate for AI agents that can safely automate engineering tasks, not just answer questions. That includes everything from “read some logs and tell me what’s wrong” to “look into a bug ticket and propose a fix PR.”</p><h3>The Initial Architecture: Internal MCP + Registry</h3><h4>Hosted, Not Local</h4><p>Although MCP supports local servers (running on your laptop or personal cloud development box, communicating over stdio), we explicitly optimized for <strong>internal cloud-hosted MCP servers</strong>, where our internal routing and security logic can best be applied.</p><p>Local MCP servers are still possible for experimentation, but the paved path is “write a server, deploy it to our cloud compute environment, list it in the registry.”</p><h4>Many Small Servers, Not One Giant One</h4><p>We debated a <strong>single monolithic MCP server</strong> vs. multiple domain-specific servers. We chose the latter: <strong>multiple MCP servers</strong> (e.g., Presto, Spark, Airflow) each own a small, coherent set of tools. This lets us apply <strong>different access controls</strong> per server and avoid crowding the model’s context.</p><p>A common piece of feedback we received early on was that spinning up a new MCP server required too much work: deployment pipelines, service configuration, and operational setup before writing any business logic. To address this, we created a unified deployment pipeline that handles infrastructure for all MCP servers: teams define their tools and the platform handles deployment and scaling of their service. This lets domain experts focus on their business logic rather than figuring out deployment mechanics.</p><h4>The Internal MCP Registry</h4><p>The <strong>MCP </strong><a href="https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/"><strong>registry</strong></a> is the source of truth for which MCP servers are approved and how to connect to them. It serves two audiences. The <strong>web UI</strong> lets humans discover servers, the owning team, corresponding support channels, and security posture. The Web UI also shows the MCP server’s live status and visible tools. The <strong>API</strong> lets AI clients (e.g., our internal AI chat platform, AI agents on our internal communications platform, IDE integrations) discover and validate servers, and lets internal services ask “Is this user allowed to use server X?” before letting an agent call into it.</p><p>This is also the backbone for governance: only servers registered here count as “approved for use in production.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aQrjPcAfUoIF-WUdyRIxBg.png" /><figcaption>Figure 1: architectural diagram of Pinterest’s MCP ecosystem.</figcaption></figure><h3>What We Shipped</h3><h4>A Growing Fleet of MCP Servers</h4><p>We started by seeding a small set of high-leverage MCP servers that solved real pain points, then let other teams build on top of that.</p><p>Representative examples (by usage):</p><ul><li><strong>Presto MCP server</strong>: consistently our highest-traffic MCP server. Presto tools let agents (including AI-enabled IDEs) pull Presto-backed data on demand so agents can bring data directly into their workflows instead of context-switching into dashboards.</li><li><strong>Spark MCP server</strong>: underpins our AI Spark debugging experience, used to diagnose Spark job failures, summarize logs, and help record structured root-cause analyses, turning noisy operational threads into reusable knowledge.</li><li><strong>Knowledge MCP server</strong>: a general-purpose knowledge endpoint (used by our internal AI bot for company knowledge and Q&amp;A and other agents to answer documentation and debugging questions across internal sources), so agents can reach for institutional knowledge with the same ease as calling a tool.</li></ul><h4>Integrations Into Pinterest Surfaces</h4><p>We didn’t want MCP to be a science project; it had to show up where engineers already work.</p><p>Our internal LLM web chat interface is used by the majority of Pinterest employees daily. The frontend automatically performs OAuth flows where required, and returns a list of usable tools for the current user, scoped to respect security policies. Once connected, our AI chat agent binds MCP tools directly into its agent toolset so invoking MCP feels no different from calling any other tool.</p><p>We also have AI bots embedded in our internal chat platform, which also exposes MCP tools. Like our LLM web chat interface, it handles authentication and authorization through the registry API. It also supports functionality such as restricting certain MCP tools to certain communication channels (for example, Spark MCP tools are only available in Airflow support channels).</p><p>An overview of the flow from starting to build an MCP server to when it’s consumed by an end user:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y5mu5OeZuuUP5PTOuFBvhg.png" /><figcaption>Figure 2: end-to-end flow of developing an MCP server</figcaption></figure><h3>Security, Governance, and Policy</h3><p>Letting AI agents call tools that <strong>touch real systems and data</strong> raises obvious security questions. We’ve treated MCP as a joint project with Security from day one.</p><h4>Security Standards and Review</h4><p>We defined a dedicated <strong>MCP Security Standard</strong>. Every MCP server that is not a one-off experiment must be tied to an owning team, appear in the <strong>internal MCP registry</strong>, and go through review, yielding Security, Legal/Privacy, and (where applicable) GenAI review tickets that must be approved before production use. This set of reviews determines the security policies that are put in place around the MCP server, such as which user groups to limit access of the server to.</p><h4>AuthN and AuthZ</h4><p>At runtime, almost every MCP call is governed by two layers of auth: <strong>end-user JWTs</strong> and <strong>mesh identities</strong>.</p><p><strong>End-user flow (JWT-based)</strong></p><ol><li>A user interacts with a surface like our web AI chat interface, an IDE plugin, or an AI bot.</li><li>The client performs an OAuth flow against our internal auth stack and sends the resulting JWT when it connects to the MCP registry and the target MCP server.</li><li>Envoy validates the JWT, maps it to X-Forwarded-User, X-Forwarded-Groups, and related headers, and enforces coarse-grained security policies (for example, “AI chat webapp in prod may talk to the Presto MCP server, but not to experimental MCP servers in dev namespaces”).</li><li>Inside the server, tools use a lightweight @authorize_tool(policy=’…”) decorator to enforce finer-grained rules (for example, only Ads-eng groups can call a get_revenue_metrics, even if the server itself is reachable from other orgs).</li></ol><p>Note that since some MCP servers can execute queries against sensitive internal data systems (like the Presto MCP server), we implemented <strong>business-group-based access gating</strong>. Rather than granting access to all authenticated Pinterest employees and contractors, some servers will:</p><ol><li>Extract business group membership from the user’s JWT token</li><li>Validate that the user belongs to an authorized group before accepting the connection (the list of approved groups is set during the initial review stage)</li><li>Selectively enable capabilities only for users whose roles require data access</li></ol><p>At Pinterest, this means that even though the Presto MCP server is technically reachable from broad surfaces like our LLM web chat interface, only a specific set of approved business groups (for example, Ads, Finance, or specific infra teams) can establish a session and run the higher-privilege tools. Turning on a powerful, data-heavy MCP server in a popular surface therefore doesn’t silently expand who can see sensitive data.</p><p>Some servers require a valid JWT even for tool discovery. That gives us user-level attribution for every invocation and a clean way to reason about “who did what” when we look at logs.</p><p><strong>Service-only flows (SPIFFE-based)</strong></p><p>For low-risk, read-only scenarios, we can rely on <strong>SPIFFE-based auth</strong> (mesh identity only). Our internal service mesh still enforces security policies, but the server authorizes based on the calling service’s mesh identity instead of a human JWT. We reserve this pattern for cases where there’s no end user in the loop and the blast radius is tightly constrained.</p><p><strong>Contrast with the MCP OAuth Standard</strong></p><p>The MCP specification defines an <a href="https://modelcontextprotocol.io/specification/draft/basic/authorization">OAuth 2.0 authorization flow</a> where users explicitly authenticate with each MCP server, typically involving consent screens and per-server token management. Our approach is different: users already authenticate against our internal auth stack when they open a surface like the AI chat interface, so we piggyback on that existing session. There is no additional login prompt or consent dialog when a user invokes an MCP tool. Envoy and our policy decorators handle authorization transparently in the background, giving us fine-grained control over who can call which tools without surfacing the complexity of per-server authorization flows to the end user.</p><h4>Human in the Loop</h4><p>Because MCP servers enable automated actions, the blast radius is larger than if a human manually wielded these tools. Our agent guidance therefore mandates <strong>human-in-the-loop</strong> before any sensitive or expensive action: agents propose actions using MCP tools, and humans approve or reject (optionally in batches) before execution. We also use <a href="https://modelcontextprotocol.io/specification/draft/client/elicitation"><strong>elicitation</strong></a> to confirm dangerous actions. In practice, this looks like our AI agents asking for confirmation before applying a change to e.g. overwrite data in a table.</p><h3>Observability and Success Metrics</h3><p>We didn’t want MCP to become a black box. From the start, we designed it to be <strong>measured and observable</strong>. All MCP servers at Pinterest use a set of library functions that provide logging for inputs/outputs, invocation counts, exception tracing, and other telemetry for impact analysis out of the box. At the ecosystem level, we measure the <strong>number of MCP servers</strong> and tools registered, the <strong>number of invocations</strong> across all servers, and the <strong>estimated time-savings per invocation</strong> provided as metadata by server owners.</p><p>These roll up into a single north-star metric: <strong>time saved</strong>. For each tool, owners provide a directional “minutes saved per invocation” estimate (based on lightweight user feedback and comparison to the prior manual workflow). Combined with invocation counts, we get an order-of-magnitude view of impact, which we treat as a directional signal of value. As of January 2025, MCP servers have ramped up to <strong>66,000 invocations per month</strong> across <strong>844 monthly active users</strong>. Using these estimates, MCP tools are saving on the order of <strong>7,000 hours per month</strong>.</p><h3>Conclusion</h3><p>In the past year, Pinterest has successfully transitioned from an initial concept to a robust, production-ready ecosystem for the Model Context Protocol (MCP). By explicitly choosing an architecture of internal cloud-hosted, multiple domain-specific MCP servers connected via a central registry, we have built a flexible and secure substrate for AI agents. These high-leverage tools are integrated directly into employees’ daily workflows, meeting them where they work.</p><p>Crucially, this entire system was built with a security-first mindset. Our two-layer authorization model using end-user JWTs and mesh identities, combined with a dedicated MCP Security Standard and business-group-based access gating on sensitive servers like Presto, ensures that powerful AI agents operate with the principles of least privilege and full auditability.</p><p>The results are clear: the MCP ecosystem has already grown to over 66,000 invocations per month, delivering an estimated 7,000 hours of time saved monthly for our engineers. This success confirms the value of using an open-source standard to unify tool access for AI.</p><p>Looking ahead, we will continue to expand the fleet of MCP servers, deepen integrations across more engineering surfaces, and refine our governance models as we empower more AI agents to safely automate complex engineering tasks, further boosting developer productivity at Pinterest.</p><h3>Acknowledgements</h3><p>This AI-enabled MCP ecosystem would not have been possible without:</p><ul><li>Nick Borgers, Kalpesh Dharwadkar, Amine Kamel from our security engineering team</li><li>Scott Beardsley, James Fish from our traffic engineering team</li><li>Leon Xu, Charlie Gu, Kingsley Ochu from our AI Agent Foundations team</li><li>Scott Herbert, Anthony Suarez, Kartik Paramasivam for their engineering sponsorship and guidance</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d881eb4c16f1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/building-an-mcp-ecosystem-at-pinterest-d881eb4c16f1">Building an MCP Ecosystem at Pinterest</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[Unified Context-Intent Embeddings for Scalable Text-to-SQL]]></title>
            <link>https://medium.com/pinterest-engineering/unified-context-intent-embeddings-for-scalable-text-to-sql-793635e60aac?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/793635e60aac</guid>
            <category><![CDATA[agentic-bi]]></category>
            <category><![CDATA[context-engineering]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[text-to-sql]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 06 Mar 2026 22:01:01 GMT</pubDate>
            <atom:updated>2026-03-09T16:45:08.451Z</atom:updated>
            <content:encoded><![CDATA[<p>Your Analysts Already Wrote the Perfect Prompt</p><p>Authors: Keqiang Li, Bin Yang</p><p>In our <a href="https://medium.com/pinterest-engineering/how-we-built-text-to-sql-at-pinterest-30bad30dabff">previous blog post</a>, we shared how Pinterest built Text-to-SQL with RAG-based table selection (Retrieval-Augmented Generation). That system introduced schema-grounded SQL generation and retrieval-augmented table selection. These were important first steps, but not enough for reliable analytics at Pinterest scale.</p><p>The challenge was fundamental: with over 100,000 analytical tables and 2,500+ analytical users across dozens of domains, simple keyword matching and table summaries were not enough. When an analyst asks “What’s the engagement rate for organic content by country?”, they need more than a list of tables with similar names. They need the system to understand <em>analytical intent</em>, the business question behind the query, and surface patterns that have actually worked for similar analyses.</p><p>This article describes how we evolved from basic Text-to-SQL to a production Analytics Agent that helps analysts discover tables, find reusable queries, and generate validated SQL from natural language. Now the most widely adopted agent at Pinterest, it was built on two key engineering choices:</p><ol><li><strong>Unified context-intent embeddings</strong> — We transform historical analyst queries into context rich, full semantic representations that capture analytical intent — the business question a query was designed to answer, rather than raw SQL syntax. This enables semantic retrieval that understands meaning, not just keywords.</li><li><strong>Structural and statistical patterns with governance-aware ranking</strong> — We extract validated join keys, filters, aggregation logic, and usage signals from query history, and combine them with governance metadata (table tiers, freshness, documentation quality) to rank results. This ensures the system surfaces not just relevant tables, but <em>trustworthy</em> ones grounded in patterns that have actually worked.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3jC8YZyfiS0t727luJGuTw.png" /></figure><h3>The Foundation: From 400K Tables to AI-Ready Data</h3><p>Before we could build an intelligent analytics assistant, we needed to solve a more basic problem: our data warehouse was a mess.</p><p>A few years ago, Pinterest’s data warehouse had <strong>hundreds of thousands of tables</strong>, most with no clear owner or documentation. Our governance roadmap called for reducing the table footprint from roughly 400K to around 100K through standardization and cleanup.</p><p>We launched a table governance and tiering program:</p><ul><li><strong>Tier 1</strong>: Cross-team, production-quality tables with strict documentation and quality requirements.</li><li><strong>Tier 2</strong>: Team-owned tables with lighter but still enforced standards.</li><li><strong>Tier 3</strong>: Everything else, including staging, temporary, and legacy tables, subject to aggressive retention and deprecation policies.</li></ul><p>With these governance constructs, PinCat, Pinterest’s internal data catalog built on open source <a href="https://datahubproject.io/">DataHub</a>, became the system of record for:</p><ul><li>Table tier tags, owners, and retention policies</li><li>Column-level semantics via <a href="https://docs.datahub.com/docs/glossary/business-glossary"><strong>glossary terms</strong></a> (reusable business concepts like user_id or pin_id)</li></ul><p>This governance work laid the groundwork for everything that followed. It gave us a clear map of “good” tables to prioritize and a structured way to express meaning at the column level, which are essential inputs for any AI system.</p><h3>Encoding Analytical Knowledge from Query History</h3><p>Here is where our approach diverges from traditional Text-to-SQL systems.</p><p>Why not just use an LLM with standard RAG? Most approaches index tables by their documentation and maybe some sample queries, then retrieve tables with semantically similar descriptions when a user asks a question. This works for simple cases, but breaks down in an environment like ours:</p><ul><li>The analytical question does not match any table description’s wording</li><li>Multiple tables could answer the question, but only specific join patterns work</li><li>The “right” way to compute a metric involves Pinterest-specific conventions</li><li>Quality signals (table tiering), authoritative schemas, and established query patterns live in different systems, so no single search retrieves all the context needed</li></ul><p>Without systematic access to how analytics is actually done at Pinterest — the tables, joins, filters, and metric definitions that analysts rely on daily, success depends on chance rather than grounded knowledge.</p><p>Our solution: encode analytical knowledge from query history along two complementary dimensions — <strong>unified context-intent embeddings</strong> that capture the meaning behind queries, and <strong>structural and statistical patterns</strong> that capture how queries are built and how well they perform.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vk70fy_LLMNMU3jQSJlOew.png" /></figure><h4><strong>Analytical Intent as Unified Context-Intent Embeddings</strong></h4><p>We convert each SQL query into a semantically rich natural-language description that captures the business question the query was designed to answer. This happens through a three-step pipeline:</p><p><strong>Step 1: Domain Context Injection</strong></p><p>Before we attempt to interpret a query, we inject Pinterest-specific semantic information alongside the raw SQL:</p><ul><li><strong>Table and column descriptions</strong> from PinCat to add business context</li><li><strong>Standardized glossary terms</strong> (e.g., “advertiser_id” maps to. g_advertiser_id in one table and adv_id in another)</li><li><strong>Metric definitions</strong> (e.g., “engaged user” means specific action types)</li><li><strong>Domain expertise</strong> such as data quality caveats or recommended date ranges</li></ul><p>At Pinterest’s scale, maintaining this context manually would be impractical. As we describe in Scaling Documentation with AI and Lineage, we use AI-generated documentation, join-based glossary propagation, and search-based semantic matching to keep this context rich and up to date automatically.</p><p>This context is critical: without it, a downstream LLM would see only raw table and column names and miss the business meaning behind them.</p><p><strong>Step 2: SQL to Text</strong></p><p>With domain context in hand, we use an LLM to translate each SQL query into a structured description of the query author’s original analytical intent. Rather than producing a simple one-line summary, the LLM generates three complementary outputs: a <strong>high-level summary</strong> that captures business purpose and domain, a set of <strong>analytical questions</strong> the query could help answer, and a <strong>detailed breakdown</strong> of the query’s logic in plain English.</p><p>Consider this ads performance query:</p><pre>SELECT<br>    keyword,<br>    SUM(impressions) AS total_impressions,<br>    SUM(revenue) / NULLIF(SUM(IF(is_first_conversion, clicks, 0)), 0) AS cpc,<br>    (SUM(revenue) / NULLIF(SUM(IF(is_first_conversion, impressions, 0)), 0)) * 1000 AS cpm<br>FROM ads.keyword_performance<br>WHERE dt BETWEEN &#39;2024-10-01&#39; AND &#39;2024-10-31&#39;<br>  AND advertiser_id = 12345<br>  AND keyword IS NOT NULL<br>GROUP BY keyword<br>ORDER BY total_impressions DESC</pre><p>Our SQL-to-text transformation produces:</p><p><strong>Summary:</strong> <em>“Extracts ad performance metrics — total impressions, CPC, and CPM by keyword for a specific advertiser. CPC and CPM are calculated based on first-conversion events, focusing on ad effectiveness in acquiring new customers.”</em></p><p><strong>Analytical questions:</strong></p><ul><li><em>What are the top-performing keywords by impressions for a given advertiser?</em></li><li><em>How cost-effective are ad campaigns based on CPC and CPM for different keywords?</em></li></ul><p><strong>Detailed breakdown:</strong> Column definitions, transformation logic (CPC derived from first-conversion revenue divided by first-conversion clicks), filters applied, and the business purpose of optimizing keyword targeting within the advertising ecosystem.</p><p>Two design choices make this process effective at scale. First, the <strong>analytical questions</strong> create a direct bridge between future user questions and indexed queries. When a new analyst asks “What’s the CPC for our top keywords?”, the system matches their question against questions it already knows how to answer — not just query descriptions. This is what enables intent-based retrieval to work across different phrasings, table names, and column structures.</p><p>Second, the descriptions are kept <strong>deliberately generalizable</strong>: the LLM strips temporal specifics (exact dates, individual IDs) while preserving business-meaningful values like metric types and entity categories. A query originally written for “October 2024 keyword performance” generalizes to match future questions about “ad CPC by keyword” regardless of date range. Together, these choices turn years of analysts’ institutional SQL knowledge into a reusable, searchable knowledge base.</p><p><strong>Step 3: Text to Embedding</strong></p><p>The natural-language description is then embedded into a vector representation. This enables <strong>intent-based retrieval</strong>: when a new question comes in, we embed it the same way and find historical queries that answered similar analytical questions, regardless of exact keyword matches. A question about “organic engagement by market” can match a query originally described as “non-promoted pin interaction rates by country” because the embeddings capture semantic similarity, not lexical overlap.</p><h4>Structural &amp; Statistical Patterns</h4><p>While analytical intent captures <em>what</em> a query means, we also need to capture <em>how</em> queries are built and <em>how well</em> they perform. We extract two categories of hard facts from query history:</p><p><strong>Structural patterns</strong> are derived by parsing SQL queries:</p><ul><li><strong>Join patterns</strong>: Which tables are joined, on which keys, and with what conditions</li><li><strong>Common filters</strong>: Typical WHERE clauses and partition filters for each table</li><li><strong>Aggregation patterns</strong>: How metrics are computed (COUNT DISTINCT vs SUM, grouping dimensions)</li><li><strong>Subquery structures</strong>: Common CTEs (Common Table Expressions) and nested query patterns for complex analyses</li></ul><p><strong>Statistical signals</strong> are aggregated from query execution metadata:</p><ul><li><strong>Table co-occurrence frequency</strong>: How often tables are queried together signals analytical relationships</li><li><strong>Query success rates</strong>: Patterns from successful queries are weighted higher than failed attempts</li><li><strong>Usage recency and volume</strong>: Recent, frequently-used patterns reflect current best practices</li><li><strong>Author expertise</strong>: Queries from experienced analysts in specific domains carry higher weight</li></ul><p>These statistical signals combine with <strong>governance metadata</strong> — table tiers, data freshness, documentation completeness, to form what we call <strong>governance-aware ranking</strong>. When retrieval returns candidate tables and patterns, the system does not rank by semantic similarity alone. It fuses similarity scores with trust signals: a Tier-1 table with active ownership and fresh data ranks higher than a semantically similar but deprecated or undocumented alternative. This ensures the system surfaces not just <em>relevant</em> tables, but <em>trustworthy</em> ones.</p><p>Together, structural patterns and governance-aware ranking form a <strong>library of validated, trusted solutions</strong> that guide query generation. When the agent generates SQL, it does not guess at join keys or filters — it uses patterns that have been <strong>actively used and validated by Pinterest analysts</strong> thousands of times, drawn from the most reliable sources in the warehouse.</p><h4>How the Two Dimensions Work Together</h4><p>These two dimensions complement each other: analytical intent enables semantic retrieval by converting queries into meaning-rich embeddings, while structural and statistical patterns provide the concrete, validated SQL building blocks needed to act on that retrieval. The following diagram illustrates how a single SQL query flows through both dimensions to produce encoded knowledge:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rVEENfxEudrFu9txjhPImA.png" /></figure><p>To see this in practice, consider a common analytical task:</p><p><strong>The user asks:</strong> <em>“What’s the engagement rate for organic Pins by country?”</em></p><p><strong>What the agent retrieves:</strong></p><ol><li><strong>Analytical Intent</strong>: By leveraging its unified context-intent embedding space, the agent can retrieve highly relevant queries based on intent semantics. This capability is robust against variations in table names, column structures, and specific filters (like “by country”), which would otherwise cause failures in traditional keyword-based search. Furthermore, the agent understands that “engagement rate” at Pinterest means specific action types (saves, clicks, closeups) divided by impressions, and “organic” excludes promoted content.</li><li><strong>Structural &amp; Statistical Patterns</strong>: Surfaces validated join keys (engagement queries typically join user_actions to pinson pin_id with specific filters for organic content), priortizes patterns from frequently-used, successful queries (98%+ success rate, high monthly usage), and applies proven aggregation logic.</li></ol><p><strong>Result</strong>: The agent generates SQL that follows established patterns, uses correct join keys, and applies domain-specific business logic — all learned from the accumulated knowledge encoded in query history.</p><h4>The Self-Reinforcing Learning Cycle</h4><p>This setup works because of a core insight: <strong>your analysts already wrote the perfect prompt</strong>. Every SQL query an analyst has ever written, the tables they chose, the joins they constructed, the filters they applied, the metrics they computed, encodes hard-won domain expertise. Traditional Text-to-SQL systems ask an LLM to figure out these patterns from scratch for every question. We instead treat query history as a vast library of expert-authored analytical solutions, and unified context-intent embeddings are the key that makes this library searchable by meaning rather than syntax.</p><p>And because every new query enriches the library, the system is self-reinforcing. As analysts across Pinterest write more queries, each one becomes a new entry in the knowledge base:</p><ul><li>New analytical patterns emerge as teams develop novel approaches to measurement</li><li>Metric calculation standards evolve and propagate across teams</li><li>Join conventions spread as validated patterns are reused</li><li>Domain-specific filters and aggregations become discoverable to analysts outside the original domain</li></ul><p>The analyst who figures out how to compute retention by acquisition channel doesn’t just answer their own question — they write a reusable recipe that any future analyst can discover by simply asking in plain English. The more analysts use the data warehouse, the more knowledge the agent absorbs, and the better it gets at helping the next analyst. In effect, every analyst at Pinterest is continuously teaching the system, making the combined expertise of over 2,500 analysts accessible to everyone rather than siloed within teams.</p><h4>Scaling Documentation with AI and Lineage</h4><p>Unified context-intent embeddings require rich documentation to inject domain context. But manual documentation alone was never going to keep pace with a warehouse of this size.</p><p>We attacked the problem on three fronts.</p><h4><strong>AI-Generated Table and Column Docs</strong></h4><p>We built <strong>AI Table Documentation</strong>, a system that uses LLMs to generate table and column descriptions from multiple signals:</p><ul><li>Data lineage - upstream and downstream tables and their documentation</li><li>Existing PinCat docs, if present</li><li>Column-level glossary terms</li><li>Representative example queries from QueryBook (Pinterest’s collaborative SQL editor, where analysts write, run, and share queries)</li></ul><p>For highly curated Tier-1 tables, we kept humans in the loop. For Tier-2 tables, we flipped the ratio: LLMs draft, humans review. All AI-generated docs are clearly marked as such in PinCat, and owners are notified to review and edit over time.</p><h4><strong>Column Semantics via Join-Based Lineage</strong></h4><p>To make documentation reusable across tables, we invested heavily in <strong>glossary term propagation</strong>, which automatically infers column semantics from join patterns:</p><ul><li>We analyzed query logs to build a <strong>join graph</strong> between columns (e.g., data.pins_d.id joining to ad.ad_video_event_flat_spark.objectid)</li><li>When a well-documented column (with a glossary term like pid_id) repeatedly joins to an undocumented column, we propagate that glossary term to the undocumented side</li></ul><p>This join-derived lineage allowed us to auto-tag thousands of columns with high-quality glossary terms.</p><h4>Search-Based Propagation</h4><p>For cases where join patterns were sparse, we complemented lineage with <strong>search-based propagation</strong>: indexing glossary terms and column docs into a vector database, enabling semantic similarity search between column descriptions and existing glossary term definitions.</p><p>Together, these efforts mean that as high-quality docs are added in one place, they automatically propagate to related columns and tables, dramatically reducing the manual documentation burden.</p><p>The results have been significant. AI-generated table descriptions reduced manual documentation effort by approximately 40%, with user surveys rating over 75% of these descriptions as “usable” or better. Join-based lineage auto-tagged over 40% of columns in scope, and combined with search-based propagation, these efforts reduced overall manual documentation work by nearly 70% while keeping humans in the loop for critical assets.</p><h4>Infrastructure: Vector DB as a Service</h4><p>Building unified context-intent embeddings and generating AI documentation both produce vectors that need to be stored, searched, and kept up to date. As more teams across Pinterest started building LLM features — table search, Text-to-SQL, AI documentation, it became clear we were all reinventing the same infrastructure: custom indexes, ad hoc ingestion jobs, and brittle retrieval logic.</p><p>To avoid a proliferation of one-off solutions, we built an internal <strong>Vector Database as a Service</strong>.</p><h4><strong>Built on OpenSearch, Integrated with Our Data Stack</strong></h4><p>After evaluating several options, we standardized on <strong>AWS OpenSearch</strong> for our internal productivity use cases. We paired it with existing infrastructure:</p><ul><li><strong>Tables</strong> as the source of truth for vectorized datasets</li><li><strong>Airflow</strong> to run index creation and ingestion DAGs</li></ul><p>Teams define a vector index via a simple JSON schema specifying the index alias, vector field dimensionality (e.g., 1536-dim embeddings), and source Hive table mappings. An Airflow workflow then validates the config, creates the index, and publishes metadata so other teams can discover and reuse existing knowledge bases.</p><h4>Scalable Indexing with Daily Updates</h4><p>The service handles <strong>millions of embeddings</strong> across tables, queries, column descriptions, and documentation, with daily incremental updates as new data assets and queries are created.</p><p>It supports hybrid patterns that combine semantic similarity (vector distance) with traditional metadata filters. For example, you can search for “tables semantically similar to user_actions that are Tier 1 and contain impression data.”</p><p>This pattern lets teams go from <strong>zero to a production-grade vector index in days instead of weeks</strong>, without having to solve embedding, ingestion, and monitoring from scratch.</p><h3>The Pinterest Analytics Agent: Putting It All Together</h3><p>With governance, documentation, query indexing, and vector infrastructure in place, we could finally build what many analysts actually wanted: <strong>a natural-language assistant that understands Pinterest’s data</strong>.</p><p>The <strong>Pinterest Analytics Agent</strong> is a specialized LLM-driven system that:</p><ul><li>Answers questions like “<em>What table should I use to analyze retention for organic content?</em>”</li><li>Generates and validates SQL from natural language</li><li>Finds and reuses existing analytical assets where possible</li></ul><p>A core design principle is the <strong>asset-first approach</strong>: the agent should surface existing, trusted assets — tables, curated queries, dashboards, metric definitions before generating new SQL. Today, this is implemented for table and query discovery; as we index more asset types, the agent progressively expands what it can surface, promoting reuse and consistency across teams.</p><h3>Architecture Overview</h3><p>The agent’s architecture has four layers:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0G9xhvQ8iX6LPwWBx0q76A.png" /></figure><p><strong>Agent Orchestration Layer</strong>: An LLM with Pinterest-specific prompts classifies tasks (documentation lookup, table discovery, query discovery, Text-to-SQL, execution) and decides which tools to call and in what order.</p><p><strong>MCP Integration Layer</strong>: A set of Model Context Protocol (MCP) tools providing a unified interface to table search (backed by vector DB + PinCat), query search (our query description index), knowledge search (internal docs), and Presto execution with EXPLAIN validation.</p><p><strong>Context Layer</strong>: The knowledge foundation, including PinCat schemas and table tiers, vector indexes of tables and queries, expert-curated docs and metric definitions, and usage patterns from query logs.</p><p><strong>Execution Layer</strong>: Presto for validated SQL with EXPLAIN-before-EXECUTE, tight LIMITs, and error-recovery loops.</p><h4>An End-to-End Query Flow</h4><p>When a user asks:</p><p>“Show me weekly retention for new users in the US over the past three months.”</p><p>The agent:</p><p><strong>1. Classifies the task as Text-to-SQL</strong></p><p><strong>2. Retrieves context in parallel</strong><br>• Table search and ranking using our knowledge base for semantic search and statistic based ranking<br>• Relevant historical queries from the query index (using unified context-intent embeddings)<br>• Table metadata from PinCat (tiers, owners, freshness)<br>• Any metric definitions or docs that mention retention</p><p><strong>3. Generates SQL with strict validation:<br></strong> • References only existing tables/columns (PinCat validation)<br>• Uses column profiling data to ensure filter values match actual data (e.g., &#39;WEB’ not &#39;web&#39;), avoiding “looks right but returns nothing” failures<br>• Reuses known join keys and filters from historical queries<br>• Runs EXPLAIN before executing; if it fails, iterates with fixes up to a bounded retry limit<br>• Enforces a conservative LIMIT (100 rows or fewer) by default</p><p><strong>4. Returns results with transparency</strong>:<br>• The SQL it ran<br>• Tables and date ranges used<br>• Source references (schemas, queries, docs)<br>• Confidence indicators or warnings (e.g., suspicious joins, empty results)</p><p>From the user’s perspective, they get <strong>a working analysis in minutes</strong>, and crucially, it is grounded in the same governed tables and metrics their teammates use, not a hallucinated subset of the warehouse.</p><h4>Resolving Conflicting Signals</h4><p>With multiple sources of context, conflicts are inevitable. A query pattern might suggest one join key while documentation recommends another. When multiple sources provide conflicting information, the agent follows a defined hierarchy:</p><ol><li><strong>Expert-curated documentation</strong> (canonical guides, metric definitions) serves as the primary source of truth for business logic</li><li><strong>Schema metadata from PinCat</strong> is authoritative for column names, types, and table structure</li><li><strong>Query patterns</strong> provide guidance but are validated against schemas before use</li><li><strong>General knowledge base</strong> supplements when specialized sources lack coverage</li></ol><p>This hierarchy ensures that carefully curated Pinterest-specific knowledge takes precedence over general information, while schema metadata provides the ultimate ground truth for what actually exists in the data warehouse. The result: the agent generates SQL that is both semantically correct (aligned with business intent) and syntactically valid (grounded in actual schemas).</p><h3>Impact and Adoption</h3><p>With the full system in production, the benefits span three areas:</p><ul><li><strong>Speed</strong>: Analysts go from question to working SQL in minutes rather than hours of table exploration and debugging.</li><li><strong>Cross-domain discovery</strong>: Query patterns developed by one team become accessible to all through the shared index.</li><li><strong>Consistency</strong>: Generated queries follow established conventions and governed tables rather than ad-hoc approaches.</li></ul><p>Early adoption has validated these benefits. Within two months of launch, the Analytics Agent already covers 40% of our analyst population, with a goal to reach 50% by year-end. It is the <strong>#1 agent at Pinterest</strong>, with 10x the usage of the next most-used agent.</p><p>Beyond the agent itself, the semantic search capabilities we built to power it have become widely adopted across the company: our MCP tools for table and query search rank among Pinterest’s most popular internal tools.</p><h3>Evaluation and What We’re Learning</h3><p>To measure the agent’s effectiveness, we built a benchmarking framework focusing on two core capabilities: finding the correct tables to answer an analytical question, and generating correct SQL. Early results show that the agent meets expectations for table discovery. SQL generation has room for improvement, and the hardest cases are teaching us where to invest next:</p><ul><li><strong>Complex analytical logic</strong>: Multi-step calculations and window functions that require chaining multiple reasoning steps</li><li><strong>Ambiguous business terms</strong>: Concepts not yet captured in documentation, where the agent must fall back on general knowledge</li><li><strong>Cross-domain queries</strong>: Analyses spanning multiple domains that may surface conflicting join patterns or metric definitions</li><li><strong>Schema evolution</strong>: Recently deprecated tables whose patterns still appear in the index</li></ul><p>We mitigate these through human review, EXPLAIN validation before execution, and continuous index updates. We continue to expand test coverage with SME-verified answers, improve our evaluation judges, and incorporate real user interactions to create more representative test cases. As the agent gains new capabilities, we will add corresponding test coverage to ensure quality across all supported functionality.</p><h3>Looking Ahead</h3><p>This multi-year journey demonstrates that effective AI-powered analytics requires <strong>systematic infrastructure investment</strong>, not just plugging an LLM into existing tools.</p><p>Several lessons have already proven out:</p><p><strong>Governance and AI reinforce each other.</strong> A disciplined tiering and documentation program made AI assistance viable; the AI systems, in turn, made large-scale governance and documentation tractable.</p><p><strong>Query history is valuable.</strong> Systematically indexing and semantically enriching queries gave us a reusable knowledge base that powers table and query search, Text-to-SQL, and documentation alike.</p><p><strong>Unified context-intent embeddings beat simple RAG.</strong> By capturing analytical intent (domain-enriched, semantically embedded query descriptions) alongside structural and statistical patterns (validated joins, filters, co-occurrence, and success rates), we achieve far higher relevance than keyword matching or simple table summaries.</p><p><strong>Specialization beats generic agents.</strong> Grounding the agent in Pinterest’s schemas, metrics, and assets through MCP tools and a rich context layer produces significantly more reliable results than a generic “LLM + search” stack.</p><p>Looking ahead, we are expanding the agent’s capabilities across several dimensions:</p><ul><li><strong>Broader asset discovery</strong>: Extending our asset-first principle beyond tables and queries to dashboards, datasets, metrics definitions, curated query libraries, and workflow artifacts, surfacing trusted, pre-existing answers before generating new queries, and making the full breadth of Pinterest’s analytical assets discoverable through natural language.</li><li><strong>Deeper product integration</strong>: Embedding the agent directly into <a href="https://www.querybook.org/">QueryBook</a> and Superset so analysts can get assistance in context, without switching tools.</li><li><strong>Richer analysis capabilities</strong>: Moving beyond SQL generation to include visualization recommendations, Python-based analysis, and the ability to create dashboards and charts directly.</li><li><strong>Interoperability with other agents</strong>: As AI assistants proliferate across the organization, enabling our analytics agent to collaborate with agents in other domains.</li></ul><p>These same foundations - governance, semantic indexing, and unified context-intent embeddings will continue to be the core of how we make Pinterest’s data understandable and useful to everyone.</p><h3>Acknowledgements</h3><p>The Analytics Agent was a cross-functional initiative spanning multiple data platform teams at Pinterest. We thank</p><ul><li>Product and Integration<br>- Laura Palmer for product leadership and testing<br>- Aaron Wang for product integration<br>- Adam Podraza for documentation and prompting</li><li>Platform and Evaluation<br>- Kingsley Ochu and Charlie Gu for LLM/Agent infrastructure support<br>- Chris Moradi for the measurement and evaluation framework<br>- Jin Hyuk Chang, Kevin Singleton and Gerardo Gonzalez for supporting Vector DB Service</li><li>Data Governance<br>- Ashish Singh, Felix Loesing, Aaron Wang, Yi Yin, Keith Regier, Bohdan Demydov for support on data governance in Pinterest to help lay the groundwork for this work</li><li>Leadership<br>- Anirudh Koul for bridging teams and resources.<br>- Aman Gairola, Bryant Xiao and Jooseong Kim for the continued support for investment in this area</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=793635e60aac" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/unified-context-intent-embeddings-for-scalable-text-to-sql-793635e60aac">Unified Context-Intent Embeddings for Scalable Text-to-SQL</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[Unifying Ads Engagement Modeling Across Pinterest Surfaces]]></title>
            <link>https://medium.com/pinterest-engineering/unifying-ads-engagement-modeling-across-pinterest-surfaces-4b5cd3d99e67?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/4b5cd3d99e67</guid>
            <category><![CDATA[monetization]]></category>
            <category><![CDATA[model-unification]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Tue, 03 Mar 2026 20:01:02 GMT</pubDate>
            <atom:updated>2026-03-03T20:01:01.891Z</atom:updated>
            <content:encoded><![CDATA[<p>Authors: Duna Zhan | Machine Learning Engineer II; Qifei Shen | Senior Staff Machine Learning Engineer; Matt Meng | Staff Machine Learning Engineer; Jiacheng Li | Machine Learning Engineer II; Hongda Shen | Staff Machine Learning Engineer</p><h3>Introduction</h3><p>Pinterest ads show up across multiple product surfaces, such as the Home Feed, Search, and Related Pins. Each surface has different user intent and different feature availability, but they all rely on the same core capability: predicting how likely a user is to engage with an ad.</p><p>Before this project, the ads engagement stack relied on three independent production models, one per surface. Although the models were initially derived from a similar design, they diverged over time in several core components, including user sequence modeling, feature crossing modules, feature representations, and training configurations. This fragmentation led to persistent operational and modeling inefficiencies:</p><ul><li>Low iteration velocity: Platform-wide improvements required duplicating work across multiple codepaths, and hyperparameters tuned for one surface often could not transfer to others.</li><li>Redundant training cost: Similar ideas had to be validated separately on each model, substantially increasing experimentation and training overhead.</li><li>High maintenance burden: Operating, debugging, and evolving three materially different systems was significantly more complex than maintaining a unified stack.</li></ul><p>These challenges motivated the development of a unified engagement framework to gradually consolidate surface-specific models while retaining the flexibility needed for each surface.</p><p>In this post, we present our approach to unifying two previously separate engagement models into a single architecture with surface-specific calibration and lightweight surface-specialized components. We also describe several efficiency optimizations such as projection layers and request-level broadcasting, which reduce infrastructure costs. Overall, the unified model not only resolves the iteration, cost, and maintenance issues described above, but also strengthens representation learning by combining complementary features and modeling choices across surfaces, leading to significant online metric improvements.</p><h3>Methodology: modeling &amp; architecture evolution</h3><h4>Unification strategy and guiding principles</h4><p>We treated model unification as a major architectural change and followed three principles to avoid common failure modes:</p><ol><li>Start simple: Establish a pragmatic baseline by merging the strongest existing components across surfaces.</li><li>Iterate incrementally: Introduce surface-aware modeling (e.g., multi-task heads, surface-specific exports) only after the baseline demonstrates clear value.</li><li>Maintain operational safety: Design for safe rollout, monitoring, and fast rollback at every step.</li></ol><p>We also set explicit milestones based on serving constraints. Since the cost of Related Pins (RP), Home Feed (HF), and Search (SR) differ substantially, we first unified Home Feed and Search (similar CUDA throughput characteristics) and expanded to Related Pins only after throughput and efficiency work stabilized.</p><h4>Baseline unified model</h4><p>As a first step, we built a baseline unified model by:</p><ul><li>Unioning features across the three surface models,</li><li>Merging existing modules into a single architecture, and</li><li>Combining training datasets across surfaces.</li></ul><p>This baseline delivered promising offline improvements, but it also materially increased training and serving cost. As a result, additional iterations were required before the model was production-ready.</p><h4>Architecture refinement for Home Feed and Search</h4><p>Because RP had a substantially higher cost profile, we focused next on unifying HF and SR. We incorporated key architectural elements from each surface such as MMoE [1] and long user sequences [2]. When applied in isolation (e.g., MMoE on HF alone, or long sequence Transformers on SR alone), these changes did not produce consistent gains, or the gain and cost trade-off was not favorable. However, when we integrated these components into a single unified model and expanded training to leverage combined HF+SR features and multi-surface training data, we observed stronger improvements with a more reasonable cost profile.</p><p>The diagram below shows the final target architecture: a single unified model that serves three surfaces, while still supporting the development of surface-specific modules (for example, surface-specific tower trees and late fusion with surface-specific modules within those tower trees). During serving, each surface-specific tower tree and its associated modules will handle only that surface’s traffic, avoiding unnecessary compute cost from modules that don’t benefit other surfaces. As a first step, the unified model currently includes only the HF and SR tower trees.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QCeZtb0wPtAGMMCRQXNWvQ.png" /></figure><h4>Surface-specific calibration</h4><p>Since the unified model serves both HF and SR traffic, calibration is critical for CTR prediction. We found that a single global calibration layer could be suboptimal because it implicitly mixes traffic distributions across surfaces.</p><p>To address this, we introduced a view type specific calibration layer, which calibrates HF and SR traffic separately. Online experiments showed this approach improved performance compared to the original shared calibration.</p><h4>Multi-task learning and surface-specific exports</h4><p>Using a single shared architecture for HF and SR CTR prediction limited flexibility and made it harder to iterate on surface-specific features and modules. To restore extensibility, we introduced a multi-task learning design within the unified model and enabled surface-specific checkpoint exports. We exported separate surface checkpoints so each surface could adopt the most appropriate architecture while still benefiting from shared representation learning.</p><p>This enabled more flexible, surface-specific CTR prediction and established a foundation for continued surface-specific iteration.</p><h4>Model and serving efficiency improvements</h4><p>Infrastructure cost is mainly driven by traffic and per-request compute, so unifying models does not automatically reduce infra spend. In our case, early unified versions actually increased latency because merging feature maps and modules made the model larger. To address this issue, we paired it with targeted efficiency work.</p><p>We simplified the expensive compute paths by using DCNv2 to project the Transformer outputs into a smaller representation before downstream crossing and tower tree layers, which reduced serving latency while preserving signal. We also enabled fused kernel embedding to improve the inference latency and TF32 to speed up training speed.</p><p>On the serving side, we reduced redundant embedding table look up work with request-level broadcasting. Instead of repeating heavy user embedding lookups for every candidate/request in a batch, we fetch embeddings once per unique user and then broadcast them back to the original request layout, keeping model inputs and outputs unchanged. The main trade-off is an upper bound on the number of unique users per batch; if exceeded, the request can fail, so we used the tested unique user number to keep the system reliable.</p><h3>Evaluation</h3><p>In offline experiments, we observed improvements across HF and SR, and validated the performance gains by online experiments. As shown in the table below, we observed significant improvements on both online and offline metrics [3].</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-etu7FaKWpF2QAJRBbUm_A.png" /></figure><h3>Conclusion</h3><p>Unifying ads engagement modeling isn’t simply a matter of replacing three separate models with one. The real objective is to build a single, cohesive framework that can share learning wherever it reliably generalizes across surfaces, while still making room for surface-specific features and behavioral nuances when they genuinely matter. At the same time, the framework has to remain efficient enough to serve at scale. Ultimately, by consolidating the core approach and eliminating repeated effort, we reduce duplicated work and put ourselves in a position to ship improvements faster and more consistently.</p><p>In the next milestone, we plan to unify the RP surface for the engagement model to create a more consistent experience and consolidate the model. The primary challenge will be model efficiency, so we will integrate additional efficiency improvements to meet our performance targets and achieve this goal.</p><h3>Acknowledgements</h3><p>This work represents a result of collaboration of the ads ranking team members and across multiple teams at Pinterest.</p><p>Engineering Teams:</p><ul><li>Ads Ranking: Yulin Lei, Randy Carlson, Erika Sun (former), Zhixuan Shao, Kungang Li</li><li>Ads ML Infra: Sihan Wang, Yuying Chen, Anton Kustov, Xinyi Zhang</li><li>Leadership: Jamieson Kerns, Ling Leng (former), Jinfeng Zhuang (former), Dongtao Liu (former), Liangzhe Chen, Degao Peng, Zhifang Liu, Caijie Zhang, Shu Zhang (former), Haoyang Li (former), Xiaofang Chen (former), Yang Tang</li></ul><h3>References</h3><p>[1] Li, Jiacheng, et al. “<a href="https://medium.com/pinterest-engineering/multi-gate-mixture-of-experts-mmoe-model-architecture-and-knowledge-distillation-in-ads-08ec7f4aa857">Multi-gate-Mixture-of-Experts (MMoE) model architecture and knowledge distillation in Ads Engagement modeling development</a>”. Pinterest Engineering Blog.</p><p>[2] Lei, Yulin, et al. “<a href="https://medium.com/pinterest-engineering/user-action-sequence-modeling-for-pinterest-ads-engagement-modeling-21139cab8f4e">User Action Sequence Modeling for Pinterest Ads Engagement Modeling</a>”. Pinterest Engineering Blog.</p><p>[3] Pinterest Internal Data, US, 2025.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4b5cd3d99e67" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/unifying-ads-engagement-modeling-across-pinterest-surfaces-4b5cd3d99e67">Unifying Ads Engagement Modeling Across Pinterest Surfaces</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>