<?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[Stories by Pinterest Engineering on Medium]]></title>
        <description><![CDATA[Stories by Pinterest Engineering on Medium]]></description>
        <link>https://medium.com/@Pinterest_Engineering?source=rss-ef81ef829bcb------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*iAV-apeVpCJ1h6Znt1AzCg.jpeg</url>
            <title>Stories by Pinterest Engineering on Medium</title>
            <link>https://medium.com/@Pinterest_Engineering?source=rss-ef81ef829bcb------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 02 Jun 2026 05:50:28 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@Pinterest_Engineering/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Making User-Sequence Data More Cost-Efficient, Faster, and Easier to Use]]></title>
            <link>https://medium.com/pinterest-engineering/making-user-sequence-data-more-cost-efficient-faster-and-easier-to-use-2a56a928cae1?source=rss-ef81ef829bcb------2</link>
            <guid isPermaLink="false">https://medium.com/p/2a56a928cae1</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[data-infrastructure]]></category>
            <category><![CDATA[pinterest]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Thu, 21 May 2026 16:01:00 GMT</pubDate>
            <atom:updated>2026-05-21T16:01:00.665Z</atom:updated>
            <content:encoded><![CDATA[<p>Authors (<em>listed alphabetically</em>)<br>Ads Feature Engineering Infra team: Ajay Venkatakrishnan, Le Zhang<br>Core ML Infra team: Eric Shang, Pihui Wei<br>ML Data team: Connor Votroubek, Yi He<br>User Understanding team: Camilo Munoz, Simin Li</p><p>If you work on ranking, retrieval, or recommendation systems, you’ve probably asked for some version of the same thing: “Give me the last N meaningful actions this user took, with the right enrichments, in a format that’s easy to train and serve ML models.”</p><p>On paper, that sounds simple. In practice, “user sequences” often become one of the most expensive and fragile parts of the ML data stack.</p><ul><li>They end up powering everything from training datasets to offline analysis and online inference, so they need to be fresh and complete at the same time.</li><li>They must remain consistent as you add new events and enrichments.</li><li>And they have to do all of this while serving latency‑sensitive production workloads.</li></ul><p>This article walks through how we redesigned our user‑sequence platform to make these sequences cheaper to run, faster to extend, and easier to debug, while still supporting demanding production use cases.</p><h3>What We Mean by “User Sequence”</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bCu-Sk_V-KRgLxdHgk1fYQ.png" /></figure><p>In this context, a user sequence is an ordered list of recent, relevant events for a user, along with the enrichments (signals) attached to each event. Here, enrichments mean all the extra signals we attach to raw events, so they’re useful for models: embeddings (for example, Pin or query representations), contextual features (such as surface, device, or country), and derived attributes or counters that describe how the user interacted with a piece of content over time.</p><p>A concrete example helps. Imagine a sequence made up of the last 500 engagements a user had with Pinterest Pins. Each event in that sequence might carry a timestamp, an action type, the surface where the action occurred, and a handful of embedding features or categorical attributes.</p><p>As a data primitive, user sequences are powerful. They capture temporal behavior instead of just aggregates like “how many clicks” over a period. They enable sequence‑aware models such as Transformers, sequence encoders, or attention‑over‑history architectures. And because they preserve fairly raw behavior, they can be reused across ranking, retrieval, exploration, anomaly detection, and other workloads.</p><p>The catch is that a high‑quality sequence is not just “the N latest events from a log table.” It is the result of a multi‑step process:</p><ul><li>Ingest events from diverse sources,</li><li>Filter down to the subset of events that matter,</li><li>Enrich each event with additional signals (embeddings, metadata, and so on), and</li><li>Finally assemble those enriched events into a stable, well‑defined sequence representation.</li></ul><p>Doing this once is easy. Doing it in a way that supports many teams, many event types, and many models over multiple years is where things get interesting.</p><h3><strong>Context: Where Sequences Show Up and Why Quality Is Hard</strong></h3><p>User sequences sit underneath almost every user-facing surface: Home feed(HF), Related Pins (RP), Search Results (SR), and many others. They power both organic products and ads across these surfaces in Pinterest, so any regression in sequence quality shows up quickly in user experience and revenue.</p><p>From an infrastructure point of view, they show up in three main places.</p><ul><li>In <strong>training datasets</strong>, offline pipelines pull long history windows of enriched events per user in order to build sequence features.</li><li>In <strong>offline analysis</strong>, data scientists dissect user behavior across sessions, surfaces, or campaigns using sequence‑level queries.</li><li>And in <strong>online inference</strong>, real‑time services fetch up‑to‑date user sequences at request time to feed ranking and retrieval models.</li></ul><p>Across these use cases, sequence quality turns out to be multi‑dimensional. <strong>Freshness</strong> measures how quickly new events and enrichments show up in the sequence. <strong>Completeness</strong> asks whether late‑arriving events, corrections, or backfills are eventually reflected. <strong>Consistent</strong> enrichment is about ensuring that the same enrichments are available across streaming and batch, and that training and serving see aligned data. <strong>Stable schemas</strong> matter as well: downstream consumers need schemas to be versioned and predictable, not silently changed.</p><p>One more constraint is that this is a multi‑tenant platform. It has to support many teams and models, each with different needs and lifecycles. That makes correctness, observability, and operability just as important as raw throughput or latency.</p><h3>Goals (and Non‑Goals)</h3><p>When we stepped back to redesign the platform, we framed the work with a small set of explicit goals and non‑goals.</p><h4>Goals</h4><ul><li><strong>Provide a consistent “events → enriched signals → sequences” contract.<br></strong>Downstream consumers such as ML engineers and data scientists should see a stable, well‑defined interface that explains how events are filtered, enriched, and assembled into sequences, independent of the underlying runtime.</li><li><strong>Improve cost‑efficiency at scale.<br></strong>The platform should reduce storage and network usage for sequence data while keeping latency and reliability appropriate for online use.</li><li><strong>Make onboarding new event types and enrichments faster and safer.<br></strong>Adding a new signal or event type should mostly look like changing configuration and a small piece of well‑scoped code, instead of standing up a new bespoke pipeline.</li><li><strong>Support both real‑time and batch production paths.<br></strong>We want low‑latency updates for serving alongside batch backfills for historical coverage and corrections, with a clear policy for how the two paths merge.</li></ul><h4>Non‑Goals</h4><ul><li>To keep the scope tractable, we did not redesign downstream models or ranking architectures; the focus is on the platform that feeds them.</li><li>We also did not change the product definition of events (what counts as a click, a save, or a conversion). Those semantics remain owned by product and logging teams.</li></ul><h3>The Core Idea: One Definition, Many Runtimes</h3><p>The key organizing principle for the redesign was simple:</p><p><strong>Define a signal or event type once, then instantiate it consistently across multiple runtimes.</strong></p><p>A <strong>signal definition</strong> captures which raw events to use, which enrichments to apply, and how to assemble enriched events into a sequence. That same definition is then consumed by three different kinds of workloads:</p><ul><li>Real‑time indexing for low‑latency updates.</li><li>Batch indexing and backfill for historical data and corrections.</li><li>Online serving for fetching sequences at inference time.</li></ul><p>This “one definition, many runtimes” approach avoids the classic split‑brain failure mode where training pipelines build sequences one way from batch tables while serving systems assemble sequences a different way from online stores. Over time, those two views naturally drift apart in subtle ways.</p><p>Instead, we rely on a single configuration surface plus a shared execution engine to keep indexing, training and serving aligned.</p><h3>Architecture Overview</h3><p>System Architecture Diagram</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3bKqPKIxoln1XdiZ9ihcKw.png" /></figure><p>At a high level, the platform is composed of six major pieces that work together.</p><ol><li><strong>Ingestion (stream and batch).<br></strong>Streaming ingestion handles real‑time events, while batch ingestion reads from data‑warehouse tables, log archives, or snapshots.</li><li><strong>Enrichment and execution layer.<br></strong>A shared execution engine turns raw events into enriched records based on configuration: filters, joins, and transforms. The same engine powers both streaming and batch pipelines.</li><li><strong>Real‑time indexer.<br></strong>A streaming job filters incoming events, converts them into a normalized representation, applies enrichments, and writes incremental updates to a time‑versioned store suitable for low‑latency reads.</li><li><strong>Batch indexer and backfill pipeline.<br></strong>Scheduled batch jobs read historical raw events, apply the same filter and enrichment definitions, and produce longer sequences along with reusable intermediate datasets for backfills and offline consumption.</li><li><strong>Columnar, time‑partitioned storage.<br></strong>Sequence data is stored in a columnar layout so models can read exactly the fields they need. Time partitioning keeps writes and scans focused on relevant windows, and the dataset layout supports both long‑sequence use cases and efficient truncation for shorter windows.</li><li><strong>Online serving API.<br></strong>Finally, a serving layer exposes a clean API for requesting user sequences by signal or feature name. It fetches the right columns from storage, performs request-time enrichments, and applies any final selection or trimming logic, such as “last N events within this time window.”</li></ol><p>From the perspective of a model or client team, this all collapses into a simple contract:</p><p>Request sequence X for user U, and you’ll get a well‑defined schema of enriched events, with a documented freshness and completeness profile.</p><h3>Design Decision 1: Configuration‑as‑Code for Sequences and Enrichments</h3><h4>What We Did</h4><p>We moved sequence and enrichment definitions into configuration‑as‑code, expressed in a regular programming language (Python) with a well‑defined schema.</p><p>Our configurations describe which sequence features exist, how they’re named, and basic metadata such as owners, retention, and lifecycle stage. Event‑type configuration describes, for each event type, which enrichments apply, what filtering logic to use, and what data sources to read from. Enrichment configuration explains how to fetch or derive additional signals (for example, embeddings) and how to map them into the event schema.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JHhqK6XYXSz7QTRyoFbpdA.png" /></figure><p>These configurations are validated, compiled into a portable JSON format, stored in managed internal object storage, and then consumed by the shared execution engine across streaming, batch and serving jobs.</p><h4>Why It Mattered</h4><p>This approach made onboarding dramatically faster. New event types or enrichments can now be added primarily through configuration, plus small, isolated pieces of code where absolutely necessary, instead of via entirely new pipelines. That significantly reduces the concept‑to‑production time for new signals.</p><p>Treating configuration as code also improved reviewability and safety. Diffs are human‑readable, code owners can review changes, rollbacks are straightforward, and version history lives in standard version control systems.</p><p>A clearer separation of concerns followed naturally. ML and product teams focus on what they want (events, features, and filters) while platform teams focus on how to execute that configuration reliably and efficiently.</p><h3>Design Decision 2 — Shared Execution Engine with Pluggable Executors</h3><h4>The Concept</h4><p>We introduced a shared execution engine responsible for reading configuration, connecting to data sources (kafka, logs, tables, feature stores), running filtering and featurization, calling enrichment services or joining against offline tables, and finally writing enriched results to storage.</p><p>Within this engine, an <strong>executor</strong> is a plugin that converts a raw event into one or more enriched records. In plain terms, the executor is the “business logic module” for a particular event type or grouping, while the execution engine handles everything around it.</p><h4>Why It Mattered</h4><p>The shared engine allowed us to reuse the same core enrichment logic in both streaming jobs that handle near‑real‑time events and batch jobs that process historical data. That minimized code duplication and reduced drift between batch and real‑time behavior.</p><h4>Practical Boundaries</h4><p>To keep the system maintainable, we drew a clear line between framework and plugin code.</p><p>Framework responsibilities include wiring data sources and sinks, handling concurrency, retries, and backpressure, and parsing and validating configuration. Executors own the business‑specific filtering and featurization logic and the mapping from raw events to normalized user‑event representations.</p><h3>Design Decision 3: Lambda Architecture for Fresh and Complete Sequences</h3><h4>The Challenge</h4><p>Sequence consumers want two things that naturally pull in opposite directions. On one hand, they need freshness: “I want this morning’s actions reflected in ranking now.” On the other hand, they care about completeness and correctness: “If late events show up tomorrow, I still want my sequences and training data to be right.”</p><p>Real‑world data is messy. Events arrive late. Enrichment sources are recomputed or corrected. Backfills introduce new historical coverage months after the fact.</p><h4>The Approach</h4><p>To balance these requirements, we adopted a lambda‑style architecture for user sequences.</p><p>A streaming path processes events as they arrive and maintains a near‑real‑time view of user sequences for online inference. A batch path periodically recomputes enriched events and sequences from raw historical data, producing long sequences and reusable datasets for backfills and offline analysis.</p><p>The two paths cooperate instead of competing. The streaming path maintains the “now” view of the world, while the batch path focuses on “fixing history” and ensuring that training and long‑term analytics see consistent, corrected data.</p><h3>Design Decision 4: Columnar, Time‑Partitioned Storage with Table Semantics</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3sjJqc4-HpUUz9P0wN_47g.png" /></figure><h4>What We Chose</h4><p>Before this redesign, we stored sequences as large, consolidated “enriched event” blobs. Every online call or offline scan had to pull the whole payload — even if a model only needed a small subset of features — so request fan‑out turned directly into heavy payload size and I/O on our storage systems.</p><p>We moved sequence storage to a columnar, time‑partitioned layout that behaves like a set of tables. Each enrichment or feature lives in its own column, and reads can select only the columns they need for a given model or analysis. Data is partitioned by time bucket so that writes and scans stay constrained to relevant partitions as history grows. Engineers can query these datasets with familiar table abstractions, which makes it easy to compare runs, versions, or backfill strategies by inspecting partitions.</p><h4>Why It Mattered</h4><p>This design improved both efficiency and operability. Columnar storage improves compression and reduces network bandwidth by avoiding wide “enriched event” blobs when only a few features are needed. Time partitioning keeps I/O bounded even as the system accumulates long histories.</p><p>Operationally, having clear table semantics makes it much easier to inspect anomalous days or event types, validate new enrichments, and compare old and new pipelines side by side.</p><h3>Migration, Rollout, and Measurement</h3><p>Redesigning a platform is one thing; migrating existing production workloads is another. We treated migration as a first‑class project.</p><h4>Migration Strategy</h4><p>We followed an event type by event type approach.</p><p>For a given event type, we first ran the new pipeline in parallel with the existing one and generated “shadow” sequences. We then compared those shadow outputs to the legacy sequences over a defined period.</p><p>Since we are regenerating the data using completely new jobs, we had to accept that the data won’t have a 100% match due to the nature of our online systems. As a result, we had to have thorough validations to prove that our new system was producing approximately the same sequences when compared to the legacy system.</p><p>We decided on a strategy of using two tiers of comparisons, an event-level comparison, which compared field-by-field of events we matched between our old and new indexing jobs, as well as a sequence-level comparison, comparing the shadow sequence output with the legacy sequence output. Alongside performing A/B experiments using our new data, these validations gave us the confidence that we could safely swap our pipelines with no impact.</p><p>Once we were confident in the behavior, we performed a controlled cutover by shifting consumers to read from the new architecture. We then iterated the same process across additional event types, steadily deprecating the legacy path.</p><h4>What We Measured and Achieved</h4><p>To stay within company policies, we only describe qualitative outcomes here.</p><p>On <strong>cost</strong>, we saw significant infrastructure cost reductions once large event types were fully migrated, primarily because of more efficient storage formats, fewer replicas where appropriate, and lower network transfer per request.</p><p>On <strong>productivity</strong>, the time to onboard new enrichments and event types dropped substantially. Most changes moved from bespoke pipeline work to configuration updates and small, composable executors.</p><p>On <strong>quality</strong>, our major recommendation surfaces saw improved engagement metrics after switching to sequences produced by the new platform, while still staying within quality and safety expectations.</p><h4>Operational Readiness</h4><p>Throughout migration and into steady state, we invested heavily in observability and operational hygiene.</p><p>We set up dashboards tracking sequence freshness and lag, event and enrichment coverage, schema drift and configuration rollout status, and serving latency and error rates..</p><p>These foundations turned out to be crucial. A platform that many teams rely on will eventually have bad days; the difference between a minor blip and a major incident often comes down to whether you can quickly see what went wrong and where.</p><h3>Future Work</h3><p>There is still plenty to improve, and many of the directions generalize beyond any single company.</p><p>We want richer self‑serve tooling so that adding new signals feels more like filling out a template than editing infrastructure code. That includes wizards for new signals, static analysis for configurations, and automated backfill orchestration for common patterns.</p><p>We are also interested in stronger correctness guarantees. Anomaly detection over both indexing and serving paths would further harden the system.</p><p>Finally, we plan to broaden coverage and add richer signals. That includes extending sequence coverage to more event types and surfaces and adding higher‑level behavioral abstractions on top of raw event sequences, such as session‑level or object‑level views. The challenge is to do that while preserving the core “events → enriched signals → sequences” contract that keeps the platform coherent.</p><h3>Acknowledgements</h3><p>A big thank you to everyone who contributed through discussions, design reviews, and recurring syncs that helped shape and unblock this work. In no particular order: <br>Alekhya Pyla, Chuxi Wang, Han Wang, Jia Zhan, Kangnan Li, Kyle Soares, Laksh Bhasin (He Him), Nilesh Gohel, Se Won Jang, Xue Xia, Yang Tang, Yi He, Anton Arboleda, Yi Pan</p><p>And thank you to Archer Liu, Haoyang Li, Hongbo Deng, Qingxian Lai, Shun-ping Chiu, and Yingjian Ding for their great management support.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2a56a928cae1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/making-user-sequence-data-more-cost-efficient-faster-and-easier-to-use-2a56a928cae1">Making User-Sequence Data More Cost-Efficient, Faster, and Easier to Use</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[An Engineer’s Guide to Better AI Skills: Implementing a Testing Process to Optimize Agent…]]></title>
            <link>https://medium.com/pinterest-engineering/an-engineers-guide-to-better-ai-skills-implementing-a-testing-process-to-optimize-agent-a000c9c9abcd?source=rss-ef81ef829bcb------2</link>
            <guid isPermaLink="false">https://medium.com/p/a000c9c9abcd</guid>
            <category><![CDATA[engineering-culture]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[agentic-coding]]></category>
            <category><![CDATA[pinterest]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Tue, 12 May 2026 16:01:00 GMT</pubDate>
            <atom:updated>2026-05-12T16:01:00.688Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>An Engineer’s Guide to Better AI Skills: Implementing a Testing Process to Optimize Agent Performance in Any Repository or Skill</strong></h3><p>Author: Daniel Reed</p><figure><img alt="An abstract ball of interlocking rings." src="https://cdn-images-1.medium.com/max/1024/1*-Q3qiFheQAa0NqWO5XYjCQ.png" /></figure><p>The tech industry is currently seeing a massive overhaul in the way we work and many are enjoying the benefits of AI agents, particularly when automating engineer workflows and serving domain-specific knowledge. However, relying on agents to consistently invoke a custom skill can be surprisingly unreliable at times.</p><p>When adopting a new skill intended to help agents write code for Pinterest’s iOS architecture (I’ll call it rx-mvvm) we discovered that sometimes our knowledge skill wasn’t being loaded into our agents. To address this, we conducted a series of tests on Pin-agent (an internal fork of OpenAI’s Codex) and Claude Code to quantify the reliability of skill invocation and identify some best practices to maximize performance. This was a direct result of observing agents struggling to meet the skills bar during architectural reviews. We found that by applying different techniques we could track and drastically improve skill invocation rates on both tested agents.</p><p><strong>How to Build A Skill Test Harness</strong></p><p>Building a reliable test harness for agent skill invocation requires three key components working in concert. The Core Tool is a Bash script that orchestrates automated testing by piping prompts to your agent and capturing verbose output logs. The core execution is simple:</p><pre>if echo &quot;$prompt&quot; | claude --print --verbose --output-format stream-json &gt; &quot;$log_file&quot; 2&gt;&amp;1; then<br>    command_success=true<br>fi</pre><p>The script runs all test cases in sequence, collecting logs for later analysis. We ran the entire suite multiple times to account for the nondeterministic nature of agents. Prompts were categorized into two categories defined as arrays:</p><p><em>Positive Cases</em> — 15 prompts covering the full spectrum of skill domains:</p><pre>CORE_PROMPTS=(<br>    &quot;load the rx-mvvm-architecture skill&quot;<br>    &quot;check if this follows rx-mvvm patterns&quot;<br>    # ... 13 more cases<br>)</pre><p><em>Negative Cases</em> — 5 general programming prompts designed to expose false positives:</p><pre>EDGE_PROMPTS=(<br>    &quot;fix this Swift compilation error&quot;<br>    &quot;write unit tests for this View&quot;<br>    &quot;refactor this function&quot;<br>    # ... 2 more cases<br>)</pre><p>We then use log parsing heuristics on the json output logfiles to detect skill invocation by searching for telltale patterns in the JSON-streamed debug output.</p><pre>skill_invoked_claude() {<br>    local log_file=&quot;$1&quot;<br><br>    if grep -q &#39;&quot;name&quot;:&quot;Skill&quot;&#39; &quot;$log_file&quot; &amp;&amp; grep -q &#39;&quot;command&quot;:&quot;rx-mvvm-architecture&quot;&#39; &quot;$log_file&quot;; then<br>        return 0<br>    elif grep -q &#39;Launching skill: rx-mvvm-architecture&#39; &quot;$log_file&quot;; then<br>        return 0<br>    else<br>        return 1<br>    fi<br>}</pre><p>The script finally tallies successes across both categories and computes three key metrics with clear formulas:</p><pre>CORE_SUCCESS_RATE=$(awk &quot;BEGIN {printf \&quot;%.1f\&quot;, ($CORE_SKILL_INVOKED / $CORE_TOTAL) * 100}&quot;)<br>EDGE_FALSE_POSITIVE_RATE=$(awk &quot;BEGIN {printf \&quot;%.1f\&quot;, ($EDGE_SKILL_INVOKED / $EDGE_TOTAL) * 100}&quot;)<br>OVERALL_ACCURACY=$(awk &quot;BEGIN {printf \&quot;%.1f\&quot;, ($TOTAL_CORRECT / $TOTAL_TESTS) * 100}&quot;)</pre><p><strong>What we learned: optimizations</strong></p><p>Our initial “vanilla” testing revealed that neither agent could guarantee 100% skill invocation, particularly when engineers used terse or ambiguous prompts. The baseline performance was an overall accuracy of 73% for Codex and 62% for Claude. This low reliability is unacceptable for critical engineering workflows.</p><p>Our research confirmed that the performance of both tools can be dramatically improved, with the increase being much greater for Codex than for Claude. We found there were many ways to improve skill invocation rates:</p><ul><li>Frontmatter description:<br> — Including more contextual information (like architectural components) in the skill description in the frontmatter YAML (the section at the top) is a great way to improve performance.<br> — This gave us measurable gains that were agnostic to agent choice</li><li>Aggressive Language:<br> — Applying aggressive, all caps commands like “YOU MUST LOAD THIS SKILL IF” in the frontmatter is another way to signal importance<br> — I personally think this is a little silly, not to mention ugly</li><li><a href="http://agents.md">AGENTS.md</a>:<br> — Adding a table of skills to the <a href="http://agents.md">AGENTS.md</a> file, along with reasons to choose to use them is another optional way to improve skill loading<br> — Teams will want to balance this against the desire to save tokens in their context window by keeping their <a href="http://agents.md">AGENTS.md</a> files small.</li><li>Combination:<br> — Applying multiple techniques concurrently is a way to compound the gains, but only if you’re a Codex user. We didn’t see these gains matched while using Claude code.<br> — We also were surprised to find that asking the agents to improve on our additions did not further improve our invocation rates– it actually went down a bit.</li></ul><p>Below is a table detailing what we found in our runs. For Codex, we used GPT 5.2-codex and for Claude we were using Opus 4.5.<br>(100 tests = 5 runs * (15 “positive” + 5 “negative”) tests)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZYM4K6hs-maURXuCX4MU4g.png" /></figure><p><strong>Conclusion</strong></p><p>I would be in remiss if I didn’t say that the test prompts we are using are intentionally terse — they’re meant to catch edge cases. This isn’t an indictment of agent skills, models or harnesses. Every single test case during every single run on both agents loaded the skill when the prompt explicitly said ‘load this skill’. The primary method of reliable skill invocation is a good plan, verbose instruction and clear intent from the developer.</p><p>The overarching lesson we learned through this process was that not only is it possible to empirically test how often we were loading the skills we expected, it’s something we should encourage, adopt and improve upon so that our agentic AI coding tools become more effective. However, even with a fully optimized skill the engineers working with AI have a responsibility to use high quality and thorough prompts. Teams should follow both of these rules to unlock the full potential of AI agents for domain specific work.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a000c9c9abcd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/an-engineers-guide-to-better-ai-skills-implementing-a-testing-process-to-optimize-agent-a000c9c9abcd">An Engineer’s Guide to Better AI Skills: Implementing a Testing Process to Optimize Agent…</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[Enhancing Ad Relevance: Integrating Real-Time Context into Sequential Recommender Models]]></title>
            <link>https://medium.com/pinterest-engineering/enhancing-ad-relevance-integrating-real-time-context-into-sequential-recommender-models-bc3a2f9b682e?source=rss-ef81ef829bcb------2</link>
            <guid isPermaLink="false">https://medium.com/p/bc3a2f9b682e</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[ads-retrieval]]></category>
            <category><![CDATA[transformers]]></category>
            <category><![CDATA[monetization]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 08 May 2026 19:01:00 GMT</pubDate>
            <atom:updated>2026-05-08T19:01:00.841Z</atom:updated>
            <content:encoded><![CDATA[<p>Huiqin Xin | Machine Learning Engineer II, Ads Vertical Modeling; Lakshmi Manoharan | Senior Machine Learning Engineer, Ads Vertical Modeling; Karthik Jayasurya | Staff Machine Learning Engineer, Ads Signals; Ziwei Guo | Senior Machine Learning Engineer, Ads Vertical Modeling; Alina Liviniuk | Machine Learning Engineer II, Ads Vertical Modeling</p><h3>Motivation: The Need for Real-Time Context</h3><p>In a previous <a href="https://medium.com/pinterest-engineering/ads-candidate-generation-using-behavioral-sequence-modeling-f9077ee1325d">post</a>, <strong>Ads Candidate Generation using Behavioral Sequence Modeling</strong>, we introduced a candidate generator (CG) that uses a Transformer-based two-tower model to leverage a user’s <em>offsite</em> conversion history — a powerful signal — to predict future interactions with advertisers and specific products. This was a significant step forward, moving beyond static interest categories to model the evolving user shopping journey.</p><p>However, a key limitation of the initial sequential model was its lack of online context information. The user embeddings were inferred offline purely from historical offsite behavior, meaning that at the moment an ad was served, the model had no knowledge of what the user was currently browsing on Pinterest. This is a crucial drawback, particularly for highly contextual surfaces like <em>Related Pins </em>and <em>Search</em>, where the user’s current Pin or search query represents a strong, immediate signal of intent. For example, on the Related Pins surface, if a user is viewing a Pin of a “vintage leather armchair,” the recommended ads should be highly relevant to that specific item, not just their general, long-term interests.</p><p>This lack of context severely limited the model’s effectiveness on these surfaces; in the previous production system, less than 1% of impressions on Related Pins were attributed to this CG, indicating its candidates struggled to survive the downstream ranking and auction stages.</p><h3>The Contextual Sequential Modeling Solution</h3><p>To overcome this challenge, we developed the <strong>Contextual Sequential Two Tower Model</strong>, an evolution of the sequential recommender model specifically designed to incorporate real-time, online context. This approach focuses on three major areas: a new model architecture, a novel training approach, and a hybrid serving flow.</p><h3>Model Architecture: Integrating the Context Layer</h3><p>The core architectural change was integrating a <strong>context layer</strong> directly into the query tower of the two-tower model.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4v_UxfqWIp0z1DdT3ucUQg.png" /><figcaption>Figure 1. Contextual Sequential two-tower model architecture</figcaption></figure><p>As shown in the diagram above, the model now concatenates the output of the original Transformer encoder (which represents historical sequence information) with the output of the new context layer. This combined representation is then fed into the final Multi-Layer Perceptron (MLP) to derive the final user embedding.</p><p>For the Related Pins surface, the context layer’s input features are derived from the <em>subject Pin</em> (the Pin the user is currently viewing), specifically using features like the aggregated embedding representations of the top <em>interest categories </em>of the subject Pin, weighted by their confidence scores.</p><p>To further personalize the model, the user representation layer was augmented with embeddings of user demographic features, such as age, country, and gender.</p><h3>Model Training with Synthetic Context</h3><p>Since real-time context is only available at serving time, we had to make the model capable of learning from this signal during offline training. The solution was to use <strong>synthetic augmented data</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qh9qH2Ze-8IMZNm4bydEvw.png" /><figcaption>Figure 2. Model training with synthetic augmented data</figcaption></figure><p>During model training, we artificially inject pseudo-context information derived from the <em>positive label</em> (the conversion event) into the input sequence. For example, by projecting the <em>interest category</em> features from the positive item, we encourage the model to retrieve items that are semantically related to the <em>context</em> associated with that user session. A high dropout rate is used in the context layer during training to ensure the model still relies on the user’s historical event sequence (the Transformer output).</p><p>We opted to use synthetic augmented data over real context data due to two main challenges:</p><ol><li>Merging onsite data with offsite data presents significant technical difficulties.</li><li>We cannot guarantee that a user has viewed ad impressions on Related Pins between two sequential offsite events.</li></ol><h3>Hybrid User Embedding Inference</h3><p>Given that the context features (e.g., subject Pin features) are only known at the ad request time (online), we adopted a <strong>hybrid model inference</strong> approach.</p><ol><li><strong>Offline Inference:</strong> The majority of the user tower (the Transformer encoder) is inferred offline, and the last hidden state of the transformer (the encoded representations of the event sequence) is stored in the feature store. This is refreshed on a daily basis for users with new offsite activity.</li><li><strong>Online Inference:</strong> The remaining part of the user tower — the context layer and the final MLP head — is computed online at serving time, taking the real-time context features and the pre-computed offline user signal as inputs.</li></ol><p>This architecture and serving flow enables the user embedding to be dynamically influenced by the real-time context, ensuring the recommendations are both personalized (from sequence) and contextually relevant.</p><h3>Results and Business Impact</h3><h4>Offline evaluation</h4><p>To assess the impact of integrating context features on the survival rate of model-retrieved ad candidates, we conducted an offline evaluation. Using logged features from real traffic ad data on Related Pins, we generated the model output embedding and calculated Recall@K, which measures the proportion of positive items found in the top-K retrieved items. Here the candidates that survived the ranking funnel and delivered to the users were considered positive items. This new model demonstrated a significant improvement, achieving a 3x to 10x increase in Recall@K compared to the production model.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7g8CDQA3segw_Q5LRGSpSQ.png" /><figcaption>Table 1. Recall@K for production model and contextual model</figcaption></figure><h3>Survival Rate &amp; Relevance</h3><p>We were able to successfully drive up the survival rate of the candidates from this CG on the Related Pins surface. The median relevance of the candidates went up by <strong>~275–300%</strong>. On the Related Pins surface overall, the ads relevance metric improved by <strong>1.08%</strong>. Furthermore, we observed a significant increase in candidate delivery, with <strong>2x</strong> more ads candidates retrieved being delivered to impression.</p><h3>Topline Business Metrics</h3><p>The improvement in candidate relevance translated into ~0.7% measurable lift in conversion-related business metrics ROAS (Return on Ad Spend). In particular, the model benefits more for top countries which account for a majority of total revenue and leads to ~1.4% ROAS lift.</p><h3>Future work</h3><p>We plan to explore several key enhancements:</p><ol><li><strong>Context Surface Expansion:</strong> A key next step is to extend the context-enhanced candidate generator to other high-stakes contextual surfaces, notably Search. This is particularly crucial for Search because maintaining high relevance between the presented ad candidates and the user’s search queries is paramount.</li><li><strong>Advanced Fusion Techniques:</strong> Move beyond simple concatenation of context layers with the sequential encoder output. We propose using <strong>cross-attention-based fusion</strong>, where the context layer embedding acts as the query and the sequence of encoded transformer outputs serves as the key/value. This approach will allow the final user-tower embedding to dynamically capture the importance of each history event based on the real-time context.</li></ol><h3>Acknowledgements</h3><p>We would like to thank Supeng Ge, Yang Liu, Richard Huang, Yu Liu, Zhuqing Zhang, Kevin Liao, Yu Gu, Wanyu Zhang, for their dedicated help; thank to Alice Wu, Leo Lu, Siping Ji, Ling Leng for their incredible support and leadership; thank to Joachim Groeger for the valuable discussion and support.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bc3a2f9b682e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/enhancing-ad-relevance-integrating-real-time-context-into-sequential-recommender-models-bc3a2f9b682e">Enhancing Ad Relevance: Integrating Real-Time Context into Sequential Recommender Models</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[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-ef81ef829bcb------2</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:01 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-ef81ef829bcb------2</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-ef81ef829bcb------2</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-ef81ef829bcb------2</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-ef81ef829bcb------2</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-ef81ef829bcb------2</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-ef81ef829bcb------2</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:01 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>
    </channel>
</rss>