<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bruno Baruffaldi</title>
    <description>The latest articles on DEV Community by Bruno Baruffaldi (@barufa).</description>
    <link>https://dev.to/barufa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078070%2F6a48e9e9-5ce5-48e6-9b87-6c0fb28e0122.jpg</url>
      <title>DEV Community: Bruno Baruffaldi</title>
      <link>https://dev.to/barufa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/barufa"/>
    <language>en</language>
    <item>
      <title>Caveman Saves Tokens by Doing Less, Not Just Saying Less</title>
      <dc:creator>Bruno Baruffaldi</dc:creator>
      <pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/barufa/caveman-saves-tokens-by-doing-less-not-just-saying-less-4kl3</link>
      <guid>https://dev.to/barufa/caveman-saves-tokens-by-doing-less-not-just-saying-less-4kl3</guid>
      <description>&lt;p&gt;Caveman promises an appealing optimization for coding agents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Same answers, 65% fewer output tokens. Brain still big. Mouth small.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The idea is simple. Models often produce unnecessary explanations, narrate their tool calls, and pad conclusions with filler. Remove that verbosity and the agent should become faster and cheaper without becoming less capable.&lt;/p&gt;

&lt;p&gt;Coding agents can consume a surprising number of tokens while inspecting repositories, running commands, and correcting their own mistakes. A small skill that removes wasted language sounds like an easy efficiency win. But a coding agent operates through a trajectory: call a tool, interpret the result, and decide whether to continue. An instruction that changes how the agent communicates can also change how it acts.&lt;/p&gt;

&lt;p&gt;To measure that effect, I ran 60 tasks across Claude Code and Codex CLI using SWE Bench Verified, Terminal Bench 2, and USACO tasks. Caveman reduced Claude Code token usage by 18% and its reported cost by 25%, but in Codex token usage fell by only 3.9%.&lt;/p&gt;

&lt;p&gt;The most important result was not how many tokens disappeared, but where they disappeared from. With Caveman enabled, Claude Code performed 17% fewer steps and made almost 20% fewer tool calls, and in Codex hidden reasoning tokens fell by 14%. Caveman didn’t merely make the agents speak less. It changed how much work they performed.&lt;/p&gt;

&lt;p&gt;And if the skill mostly works by making the agent do less, adjusting the reasoning effort might be a better choice. Claude Code exposes reasoning effort as a native setting, and in this experiment medium effort reached roughly the same Pass@3 as Caveman with high effort while costing about 20% less. A style instruction that arrives at a similar operating point indirectly is harder to justify when a direct knob is one flag away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Caveman Promises
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;Caveman&lt;/a&gt; is a skill for Claude Code, Codex, Gemini, and other coding agents. It instructs the model to avoid narration, drop hedging, and communicate in compact fragments.&lt;/p&gt;

&lt;p&gt;Its README summarizes the intended behavior with a memorable distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Brain still big. Mouth small.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The main benchmark compares ten standalone answers, including prompts such as explaining Git rebase, React rerenders, and Docker multistage builds. The reported average falls from about 1,200 output tokens to under 300, the headline reduction of 65%.&lt;/p&gt;

&lt;p&gt;That result is plausible for those prompts. They ask for prose, and Caveman aggressively compresses prose. The problem is the extrapolation.&lt;/p&gt;

&lt;p&gt;A single conversational answer and a repository level coding task have very different token distributions. In a coding agent, useful output is source code, patches, and exact error messages. Caveman leaves much of that unchanged.&lt;/p&gt;

&lt;p&gt;The project itself acknowledges this distinction. Its documentation warns that Caveman only targets output tokens, adds input tokens of its own, and may become net negative when the original model is already concise. It also describes the honest evaluation control as Caveman versus a simple terse instruction, rather than Caveman versus an unconstrained baseline.&lt;/p&gt;

&lt;p&gt;Those caveats are good. Caveman is free, open source, and doesn’t collect telemetry. The issue is that the headline claims are much broader than the evidence supporting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Experiment
&lt;/h2&gt;

&lt;p&gt;I evaluated five agent configurations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Treatment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-baseline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code with Sonnet 5&lt;/td&gt;
&lt;td&gt;High effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-caveman&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code with Sonnet 5&lt;/td&gt;
&lt;td&gt;Caveman injected at session start (high effort)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code with Sonnet 5&lt;/td&gt;
&lt;td&gt;Medium reasoning effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex-baseline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex CLI with GPT 5.6 Terra&lt;/td&gt;
&lt;td&gt;High effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex-caveman&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex CLI with GPT 5.6 Terra&lt;/td&gt;
&lt;td&gt;Caveman forcibly activated (high effort)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The benchmark contained 60 tasks: 20 from SWE Bench Verified, 30 from USACO, and 10 from Terminal Bench 2. Each task was executed three times under every configuration, for 900 trials in total.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;claude-medium&lt;/code&gt; configuration is particularly important. Comparing only baseline against Caveman mixes together two possible effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Caveman may compress communication.&lt;/li&gt;
&lt;li&gt;Caveman may cause the model to spend less effort.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The medium effort arm provides a native way to reduce model effort without installing Caveman. It lets us ask whether the skill offers something beyond indirectly making the agent do less work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Claude Code showed substantial savings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Caveman&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens&lt;/td&gt;
&lt;td&gt;1,595,199&lt;/td&gt;
&lt;td&gt;1,302,271&lt;/td&gt;
&lt;td&gt;18.4% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;19,860&lt;/td&gt;
&lt;td&gt;16,059&lt;/td&gt;
&lt;td&gt;19.1% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reported cost&lt;/td&gt;
&lt;td&gt;$1.003&lt;/td&gt;
&lt;td&gt;$0.758&lt;/td&gt;
&lt;td&gt;24.5% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steps&lt;/td&gt;
&lt;td&gt;25.5&lt;/td&gt;
&lt;td&gt;21.2&lt;/td&gt;
&lt;td&gt;17.0% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;24.6&lt;/td&gt;
&lt;td&gt;19.7&lt;/td&gt;
&lt;td&gt;19.7% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent prose characters&lt;/td&gt;
&lt;td&gt;2,956&lt;/td&gt;
&lt;td&gt;1,741&lt;/td&gt;
&lt;td&gt;41.1% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@1&lt;/td&gt;
&lt;td&gt;81.1%&lt;/td&gt;
&lt;td&gt;80.0%&lt;/td&gt;
&lt;td&gt;1.1 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@3&lt;/td&gt;
&lt;td&gt;91.7%&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;5.0 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Codex showed a much smaller reduction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Caveman&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens&lt;/td&gt;
&lt;td&gt;418,852&lt;/td&gt;
&lt;td&gt;402,402&lt;/td&gt;
&lt;td&gt;3.9% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;5,252&lt;/td&gt;
&lt;td&gt;4,757&lt;/td&gt;
&lt;td&gt;9.4% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;2,542&lt;/td&gt;
&lt;td&gt;2,200&lt;/td&gt;
&lt;td&gt;13.5% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reported cost&lt;/td&gt;
&lt;td&gt;$0.257&lt;/td&gt;
&lt;td&gt;$0.249&lt;/td&gt;
&lt;td&gt;3.2% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steps&lt;/td&gt;
&lt;td&gt;17.8&lt;/td&gt;
&lt;td&gt;17.5&lt;/td&gt;
&lt;td&gt;1.8% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;11.8&lt;/td&gt;
&lt;td&gt;11.5&lt;/td&gt;
&lt;td&gt;2.7% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@1&lt;/td&gt;
&lt;td&gt;89.4%&lt;/td&gt;
&lt;td&gt;85.0%&lt;/td&gt;
&lt;td&gt;4.4 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@3&lt;/td&gt;
&lt;td&gt;93.3%&lt;/td&gt;
&lt;td&gt;90.0%&lt;/td&gt;
&lt;td&gt;3.3 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If we stop here, Caveman looks like a reasonable tradeoff for Claude and a poor one for Codex. Claude saves approximately one quarter of its modeled cost while losing little Pass@1, while Codex gives up considerably more performance for a 3.2% cost reduction.&lt;/p&gt;

&lt;p&gt;But these aggregated numbers hide the most important part of the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wrong Denominator
&lt;/h2&gt;

&lt;p&gt;Caveman promotes a reduction in output tokens. Coding agent cost, however, isn’t dominated by visible prose.&lt;/p&gt;

&lt;p&gt;For Claude Code, the baseline token distribution looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit rate&lt;/td&gt;
&lt;td&gt;97.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens as a share of all tokens&lt;/td&gt;
&lt;td&gt;1.24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible prose as a share of output tokens&lt;/td&gt;
&lt;td&gt;3.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible prose as a share of all tokens&lt;/td&gt;
&lt;td&gt;0.046%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Directly generated prose accounts for only about 0.05% of Claude’s reported session token volume.&lt;sup id="fnref:prose"&gt;1&lt;/sup&gt; A 41% reduction therefore removes only about 0.02% of the total tokens directly.&lt;/p&gt;

&lt;p&gt;Shorter replies also make subsequent contexts slightly smaller, so the full mechanical saving from prose compression is larger than 0.02%. But even a deliberately generous fixed trajectory estimate puts that effect well below 1% of total token volume. Most output tokens are code, diffs, and commands that must remain exact. That’s nowhere near the observed 18% reduction.&lt;/p&gt;

&lt;p&gt;The missing savings have to come from somewhere else. The trajectory data provides the answer: with Caveman, Claude took 17% fewer steps and made almost 20% fewer tool calls. Each removed turn avoided processing a large context again.&lt;/p&gt;

&lt;p&gt;The expensive part of an agent is taking another turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decomposing the Reduction
&lt;/h2&gt;

&lt;p&gt;The total token count can be decomposed approximately into two factors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total tokens = number of steps × tokens per step

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applying that decomposition gives the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Total tokens&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Tokens per step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codex baseline to Caveman&lt;/td&gt;
&lt;td&gt;3.9% lower&lt;/td&gt;
&lt;td&gt;1.8% lower&lt;/td&gt;
&lt;td&gt;2.2% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude baseline to Caveman&lt;/td&gt;
&lt;td&gt;18.4% lower&lt;/td&gt;
&lt;td&gt;17.0% lower&lt;/td&gt;
&lt;td&gt;1.7% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude baseline to medium&lt;/td&gt;
&lt;td&gt;37.1% lower&lt;/td&gt;
&lt;td&gt;20.7% lower&lt;/td&gt;
&lt;td&gt;20.6% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Claude, approximately 92% of the token reduction is explained by taking fewer steps. &lt;strong&gt;Tokens per step barely changed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Caveman certainly changed the writing style. Visible prose fell by 41%, and prose per step also decreased. But this compressed prose was too small to explain the session level savings. The savings appeared because the model ended its trajectory earlier.&lt;/p&gt;

&lt;p&gt;That interpretation is also visible in the tool usage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Claude baseline to Caveman&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Steps&lt;/td&gt;
&lt;td&gt;17.0% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;19.7% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell calls&lt;/td&gt;
&lt;td&gt;19.0% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent execution time&lt;/td&gt;
&lt;td&gt;15.6% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A style instruction propagated into the action policy of the agent. This doesn’t automatically mean that the removed steps were useful. Agents can overinspect repositories, repeat tests unnecessarily, and continue reasoning after they already have a valid solution. Some trajectories should be shorter.&lt;/p&gt;

&lt;p&gt;But it does mean that Caveman shrinks the brain along with the mouth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Control That Changes the Conclusion
&lt;/h2&gt;

&lt;p&gt;The strongest argument against using Caveman isn’t the small drop in quality. It’s the &lt;code&gt;claude-medium&lt;/code&gt; control.&lt;/p&gt;

&lt;p&gt;Both configurations reached exactly the same Pass@3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Caveman: 86.7%
Claude medium: 86.7%

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Caveman used 30% more tokens and cost 23% more than medium.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv19egzpoz403gyjd6cpi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv19egzpoz403gyjd6cpi.png" alt="Scatter plot comparing mean cost per trial and Pass@3 for Claude Code and Codex. Claude medium and Claude Caveman reach the same 86.7 percent Pass@3, but Caveman costs ~20 percent more." width="800" height="482"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Mean cost per trial versus &lt;a href="mailto:Pass@3"&gt;Pass@3&lt;/a&gt;. Better configurations move upward and to the left. Claude medium reaches the same Pass@3 as Claude Caveman at ~20% lower cost.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The chart makes the relevant comparison visible. On this plane, a configuration is dominated when another configuration achieves at least the same quality at a lower cost. Claude medium sits directly to the left of Claude Caveman: same Pass@3, lower cost.&lt;/p&gt;

&lt;p&gt;The difference remains after accounting for successful trials:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Medium&lt;/th&gt;
&lt;th&gt;Caveman&lt;/th&gt;
&lt;th&gt;Caveman overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pass@1&lt;/td&gt;
&lt;td&gt;77.2%&lt;/td&gt;
&lt;td&gt;80.0%&lt;/td&gt;
&lt;td&gt;2.8 points higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@3&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per resolved trial&lt;/td&gt;
&lt;td&gt;1.30 million&lt;/td&gt;
&lt;td&gt;1.63 million&lt;/td&gt;
&lt;td&gt;25.3% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per resolved trial&lt;/td&gt;
&lt;td&gt;$0.749&lt;/td&gt;
&lt;td&gt;$0.874&lt;/td&gt;
&lt;td&gt;16.6% more&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality comparison between Caveman and medium is inconclusive at Pass@1: Caveman was 2.8 points higher, but the difference was not statistically significant. The efficiency comparison is much clearer. Both configurations reached the same 87% Pass@3, yet Caveman consumed 25% more tokens per resolved trial and cost 17% more.&lt;/p&gt;

&lt;p&gt;This comparison changes the practical recommendation. Caveman can reduce resource use, but it does so by indirectly altering the agent’s trajectory. When Claude already provides a direct control for effort, using a global style instruction to reach a similar operating point is difficult to justify.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Codex Saved Less
&lt;/h2&gt;

&lt;p&gt;Caveman reduced Claude token usage by 18%, but reduced Codex usage by only 3.9%. One possible explanation is that Codex had much less waste available to remove.&lt;/p&gt;

&lt;p&gt;Before applying Caveman, the Codex and Terra combination already used substantially shorter and cheaper trajectories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Baseline metric&lt;/th&gt;
&lt;th&gt;Claude Code with Sonnet 5&lt;/th&gt;
&lt;th&gt;Codex with Terra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per trial&lt;/td&gt;
&lt;td&gt;1,595,199&lt;/td&gt;
&lt;td&gt;418,852&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steps per trial&lt;/td&gt;
&lt;td&gt;25.5&lt;/td&gt;
&lt;td&gt;17.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per step&lt;/td&gt;
&lt;td&gt;62,600&lt;/td&gt;
&lt;td&gt;23,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@1&lt;/td&gt;
&lt;td&gt;81.1%&lt;/td&gt;
&lt;td&gt;89.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass@3&lt;/td&gt;
&lt;td&gt;91.7%&lt;/td&gt;
&lt;td&gt;93.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Codex reported around 74% fewer tokens while obtaining slightly better resolution rates. This difference appeared at two levels: Codex took around 30% fewer steps, and each step processed around 62% fewer tokens.&lt;/p&gt;

&lt;p&gt;That doesn’t prove that GPT 5.6 Terra is intrinsically more efficient than Sonnet 5. The comparison mixes the model with the scaffold, the tokenizer, and everything else that differs between two harnesses.&lt;/p&gt;

&lt;p&gt;The defensible conclusion is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this harness, Codex with Terra started from a much more token efficient trajectory than Claude Code with Sonnet 5.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This provides a plausible explanation for the different Caveman results: Claude offered a long trajectory that the skill could truncate, while Codex already operated closer to a floor.&lt;/p&gt;
&lt;h3&gt;
  
  
  Caveman Needs Waste to Remove
&lt;/h3&gt;

&lt;p&gt;An exploratory analysis inside the Codex results supports this interpretation.&lt;/p&gt;

&lt;p&gt;I divided the tasks into four groups according to their baseline token consumption. Caveman increased median consumption among the cheaper tasks and only produced savings among the more expensive ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Baseline consumption group&lt;/th&gt;
&lt;th&gt;Median token change with Caveman&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First quartile&lt;/td&gt;
&lt;td&gt;27% higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second quartile&lt;/td&gt;
&lt;td&gt;11% higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third quartile&lt;/td&gt;
&lt;td&gt;13% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fourth quartile&lt;/td&gt;
&lt;td&gt;9.5% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This analysis shouldn’t be treated as causal. The groups mix datasets, contain relatively few tasks, and are defined using observed baseline consumption. Regression to the mean may explain part of the pattern.&lt;/p&gt;

&lt;p&gt;Still, the result is consistent with a floor effect. When the original trajectory is already compact, Caveman has little redundant work to remove. Its additional prompt and behavioral changes may cost more than they save. When a trajectory becomes unusually expensive, Caveman has more room to terminate it early.&lt;/p&gt;

&lt;p&gt;The 3.9% aggregate reduction in Codex was therefore driven by the expensive tail. For the median Codex task, Caveman didn’t save tokens.&lt;/p&gt;

&lt;p&gt;This suggests a better mental model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Caveman behaves like an indirect effort limiter whose effectiveness depends on how inefficient the baseline agent already is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Quality
&lt;/h2&gt;

&lt;p&gt;Caveman didn’t improve aggregate Pass@1 or Pass@3 in any of the main comparisons.&lt;/p&gt;

&lt;p&gt;For Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass@1: 81.1% → 80.0%
Pass@3: 91.7% → 86.7%

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass@1: 89.4% → 85.0%
Pass@3: 93.3% → 90.0%

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The direction is consistent, but the experiment isn’t large enough to establish the magnitude precisely.&lt;/p&gt;

&lt;p&gt;Bootstrap confidence intervals computed at the task level all crossed zero:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Pass@3 change&lt;/th&gt;
&lt;th&gt;95% confidence interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude baseline to Caveman&lt;/td&gt;
&lt;td&gt;5.0 points lower&lt;/td&gt;
&lt;td&gt;11.7 lower to 1.7 higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude baseline to medium&lt;/td&gt;
&lt;td&gt;5.0 points lower&lt;/td&gt;
&lt;td&gt;11.7 lower to 1.7 higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude medium to Caveman&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;td&gt;8.3 lower to 8.3 higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex baseline to Caveman&lt;/td&gt;
&lt;td&gt;3.3 points lower&lt;/td&gt;
&lt;td&gt;10.0 lower to 3.3 higher&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;We found no evidence that Caveman improves task resolution. Every aggregate quality comparison moved in the negative direction, but the experiment was not large enough to estimate effects of a few percentage points precisely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is one statistically detectable behavioral effect. Codex reasoning tokens fell by 14%, with a 95% confidence interval from 4.6% to 20% lower. Caveman doesn’t leave reasoning untouched, whatever the README says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Evaluations
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://blog.jetbrains.com/ai/2026/07/speak-to-ai-agents-like-cavemen-tosave-tokens/" rel="noopener noreferrer"&gt;JetBrains evaluation&lt;/a&gt; tested Caveman on Claude Code using Harbor and SkillsBench.&lt;/p&gt;

&lt;p&gt;It measured an 8.5% reduction in output tokens, far below the advertised 65%, with no detectable quality difference. Its first ten task evaluation had suggested savings near 30%; the number fell after expanding the benchmark. Its explanation was structural: coding agent output is dominated by code, diffs, tool invocations, and exact strings that Caveman can’t safely compress.&lt;/p&gt;

&lt;p&gt;That result is compatible with this experiment, but the medium effort arm adds an important control. Even when Caveman reduces a trajectory without producing a detectable quality loss, it may still be an inefficient way to reach that operating point.&lt;/p&gt;

&lt;p&gt;Other evaluations tested an even simpler question: does Caveman outperform asking the model to “be brief”?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.maxtaylor.me/articles/i-benchmarked-caveman-against-two-words" rel="noopener noreferrer"&gt;Max Taylor&lt;/a&gt; evaluated 24 prompts with prompt specific quality rubrics and found Caveman in roughly the same token and quality range as a two word concision instruction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexriosme.substack.com/p/there-is-a-reason-that-caveman-is" rel="noopener noreferrer"&gt;Alex Rios&lt;/a&gt; described the problem as the &lt;em&gt;Terse Control Fallacy&lt;/em&gt;: comparing a sophisticated compression technique against silence instead of comparing it against the trivial alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Brevity Preprint
&lt;/h2&gt;

&lt;p&gt;The Caveman README references the preprint &lt;a href="https://arxiv.org/abs/2604.00025" rel="noopener noreferrer"&gt;Brevity Constraints Reverse Performance Hierarchies in Language Models&lt;/a&gt; as evidence that shorter answers can sometimes improve accuracy.&lt;/p&gt;

&lt;p&gt;The paper evaluated 31 models on 1,485 questions and identified 115 cases where smaller models systematically outperformed larger ones. The authors associate part of this behavior with overthinking: larger models sometimes elaborate beyond what’s useful and introduce additional opportunities for error. On this selected subset, constraining responses improved large model accuracy from 40% to 67%.&lt;/p&gt;

&lt;p&gt;This is a meaningful result, but also a targeted one. The brevity intervention was evaluated on problems already exhibiting unusual scaling behavior, rather than across the benchmark as a whole.&lt;/p&gt;

&lt;p&gt;There’s also an important difference in settings. The study evaluates standalone questions: no tools, no repository, no decision about whether to keep exploring. A coding agent must repeatedly decide what to do next.&lt;/p&gt;

&lt;p&gt;The paper doesn’t validate Caveman as an agent optimization. At most, it shows that excessive elaboration can hurt on a selected group of standalone reasoning questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-Dataset Breakdown
&lt;/h2&gt;

&lt;p&gt;The aggregate results also hide substantial variation across task types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset and agent&lt;/th&gt;
&lt;th&gt;Token change&lt;/th&gt;
&lt;th&gt;Pass@1 change&lt;/th&gt;
&lt;th&gt;Pass@3 change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SWE Bench with Claude&lt;/td&gt;
&lt;td&gt;28.8% lower&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;td&gt;5.0 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USACO with Claude&lt;/td&gt;
&lt;td&gt;14.9% lower&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;td&gt;3.3 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal Bench with Claude&lt;/td&gt;
&lt;td&gt;4.1% higher&lt;/td&gt;
&lt;td&gt;6.7 points lower&lt;/td&gt;
&lt;td&gt;10.0 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWE Bench with Codex&lt;/td&gt;
&lt;td&gt;5.2% lower&lt;/td&gt;
&lt;td&gt;11.7 points lower&lt;/td&gt;
&lt;td&gt;5.0 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal Bench with Codex&lt;/td&gt;
&lt;td&gt;4.0% lower&lt;/td&gt;
&lt;td&gt;3.3 points lower&lt;/td&gt;
&lt;td&gt;10.0 points lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USACO with Codex&lt;/td&gt;
&lt;td&gt;1.5% higher&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row compares that agent’s Caveman run against its own baseline, broken out by dataset.&lt;/p&gt;

&lt;p&gt;SWE Bench with Claude was Caveman’s strongest case. It preserved Pass@1 while reducing token usage by almost 29%.&lt;/p&gt;

&lt;p&gt;Terminal Bench with Claude was the opposite. Token usage increased while both success metrics fell, but that subset contained only 10 tasks, so a difference of 10 Pass@3 points represents a single task. One contrast in that subset produced a token ratio above 100% because of one unusual task.&lt;/p&gt;

&lt;p&gt;USACO with Codex showed no quality degradation, but the baseline already reached 100% &lt;a href="mailto:Pass@3"&gt;Pass@3&lt;/a&gt;. That subset had little room to distinguish the configurations.&lt;/p&gt;

&lt;p&gt;The result depends on the agent, the model, and the dataset. Probably on the individual task too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This experiment has several limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The runs used Harbor as the evaluation harness. The reported cost is modeled rather than billed because they used subscription authentication rather than API keys.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timed out runs didn’t report token or cost metrics. The estimated savings should therefore be treated as approximate rather than exact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Claude and Codex receive Caveman through different integration mechanisms. That reflects how the skill is distributed for those tools, but it means cross agent comparisons mix the skill with its delivery path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The experiment needs more tasks to estimate small quality effects. Most tasks were solved in all three attempts or failed in all three, and only about ten of the sixty produced mixed results within a configuration, so three trials don’t provide three independent observations. With sixty tasks and few discordant pairs, differences around five percentage points remain uncertain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The medium arm controls for cost and effort, but not for style. A stronger future design would include a plain instruction such as “communicate briefly, but do not change your exploration or verification behavior.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should You Use Caveman?
&lt;/h2&gt;

&lt;p&gt;I’d use Caveman primarily as a style preference. If you find terse status updates and compact final answers easier to read, that alone may be a good reason to enable it.&lt;/p&gt;

&lt;p&gt;Enabling it, though, does more than change the wording. Caveman reaches into the trajectory itself: how much the agent reasons, how many tools it calls, and when it decides the task is done. If you’re worried about quality loss, raising reasoning effort may compensate, but that spends back the tokens Caveman saved.&lt;/p&gt;

&lt;p&gt;If the goal is reducing cost, I’d prefer explicit controls such as reasoning effort or context management. These mechanisms target resource usage directly and are easier to evaluate.&lt;/p&gt;

&lt;p&gt;Caveman makes an agent more concise. Making it more efficient is a different claim, and this experiment doesn’t support it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Lesson
&lt;/h2&gt;

&lt;p&gt;The most interesting result isn’t specific to Caveman.&lt;/p&gt;

&lt;p&gt;A system prompt is part of an agent’s policy.&lt;/p&gt;

&lt;p&gt;Instructions about tone, confidence, or verbosity may look cosmetic, but they can influence more than the text an agent produces. Because the agent repeatedly conditions its next action on those instructions, a request for concision can also affect how long it explores, how many tools it uses, and when it decides that the task is finished.&lt;/p&gt;

&lt;p&gt;That’s what happened here. Caveman did save tokens in Claude Code, but most of the reduction came from shorter trajectories. Tokens per step barely moved. Codex, which already started from a shorter and cheaper trajectory, had much less room for that effect.&lt;/p&gt;

&lt;p&gt;This is also why explicit controls matter. When the goal is to trade reasoning effort for cost, a native effort setting targets that tradeoff directly. In this experiment, medium reasoning reached the same Pass@3 as Caveman while using fewer tokens and costing less.&lt;/p&gt;

&lt;p&gt;The broader lesson is simple: instructions that look like presentation choices can become behavioral interventions once they’re placed inside an agent loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Work
&lt;/h2&gt;

&lt;p&gt;There are several ways to extend this experiment.&lt;/p&gt;

&lt;p&gt;The first is simply scale. Sixty tasks were enough to reveal clear behavioral differences, but not enough to estimate small changes in resolution rate precisely. A larger evaluation would reduce that uncertainty and make dataset specific effects easier to distinguish. It would also be interesting to include newer and harder benchmarks such as DeepSWE.&lt;/p&gt;

&lt;p&gt;The second is model diversity. This experiment only covers Claude Code and Codex with one model family each. Repeating it with models from other families, such as DeepSeek, Kimi, or Muse Spark, would help determine whether Caveman’s effects depend on the underlying model. The large difference observed between Claude Code and Codex already suggests that its impact may depend strongly on both the model and the agent scaffold.&lt;/p&gt;

&lt;p&gt;The most important control, however, would be a minimal terse control. Just something like &lt;code&gt;be concise&lt;/code&gt; or &lt;code&gt;be brief&lt;/code&gt; added to the system prompt. A stronger follow up experiment would therefore compare baseline, a minimal terse control, Caveman, and native effort controls across more tasks and model families. That would make it possible to separate style, effort, and agent behavior much more cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Caveman repository: &lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;https://github.com/JuliusBrussee/caveman&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Caveman evaluation directory: &lt;a href="https://github.com/JuliusBrussee/caveman/tree/main/evals" rel="noopener noreferrer"&gt;https://github.com/JuliusBrussee/caveman/tree/main/evals&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JetBrains evaluation: &lt;a href="https://blog.jetbrains.com/ai/2026/07/speak-to-ai-agents-like-cavemen-tosave-tokens/" rel="noopener noreferrer"&gt;https://blog.jetbrains.com/ai/2026/07/speak-to-ai-agents-like-cavemen-tosave-tokens/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Max Taylor, &lt;em&gt;I benchmarked caveman against two words&lt;/em&gt;: &lt;a href="https://www.maxtaylor.me/articles/i-benchmarked-caveman-against-two-words" rel="noopener noreferrer"&gt;https://www.maxtaylor.me/articles/i-benchmarked-caveman-against-two-words&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alex Rios, &lt;em&gt;There is a reason that caveman is a character from the past&lt;/em&gt;: &lt;a href="https://alexriosme.substack.com/p/there-is-a-reason-that-caveman-is" rel="noopener noreferrer"&gt;https://alexriosme.substack.com/p/there-is-a-reason-that-caveman-is&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Brevity Constraints Reverse Performance Hierarchies in Language Models&lt;/em&gt;: &lt;a href="https://arxiv.org/abs/2604.00025" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2604.00025&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How Do AI Agents Spend Your Money? Analyzing and Predicting Token Consumption in Agentic Coding Tasks&lt;/em&gt;: &lt;a href="https://arxiv.org/abs/2604.22750" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2604.22750&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The prose estimate converts visible agent text into an approximate token count. ↩
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>claude</category>
    </item>
    <item>
      <title>Training with scikit-learn, Deploying with the Right Runtime</title>
      <dc:creator>Bruno Baruffaldi</dc:creator>
      <pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/barufa/training-with-scikit-learn-deploying-with-the-right-runtime-4m3h</link>
      <guid>https://dev.to/barufa/training-with-scikit-learn-deploying-with-the-right-runtime-4m3h</guid>
      <description>&lt;h2 id="compiling-scikit-learn-trees-to-source-code-deploying-ml-with-the-right-runtime"&gt;Compiling scikit-learn Trees to Source Code: Deploying ML with the Right Runtime&lt;/h2&gt;

&lt;p&gt;Deploying a machine learning model is not the same problem as training it.&lt;/p&gt;

&lt;p&gt;Training environments are built for flexibility. They optimize for experimentation, rapid iteration, rich APIs, and numerical breadth. Deployment environments optimize for something very different: latency, memory footprint, startup time, packaging simplicity, and operational cost.&lt;/p&gt;

&lt;p&gt;In this post I use decision trees and random forests as a controlled simplification of a broader issue: how we deploy ML models in production. Trees are useful here precisely because their inference logic is easy to reason about. They let us isolate the systems problem without hiding behind heavy linear algebra or GPU acceleration. The point is not that trees are special. The point is that deployment deserves its own engineering decisions.&lt;/p&gt;

&lt;p&gt;TreeCompiler is a small proof of concept built around that idea. It compiles scikit-learn DecisionTreeClassifier and RandomForestClassifier models into standalone Python or Go source code. No scikit-learn. No numpy. No scientific stack at inference time.&lt;/p&gt;

&lt;p&gt;The goal is not to replace frameworks. The goal is to make explicit a systems principle: inference deserves its own runtime.&lt;/p&gt;





&lt;h2 id="a-quick-reminder-how-trees-and-forests-work"&gt;A Quick Reminder: How Trees and Forests Work&lt;/h2&gt;

&lt;p&gt;A decision tree recursively partitions the feature space using axis-aligned splits. At each internal node, a single feature and threshold are chosen to reduce impurity (for example using Gini or entropy). Inference is simply walking the tree from root to leaf by evaluating comparisons like &lt;code class="language-plaintext highlighter-rouge"&gt;x[j] &amp;lt;= threshold&lt;/code&gt;. A random forest is an ensemble of such trees trained on bootstrapped samples and random feature subsets. At inference time, each tree produces a prediction (or probability vector), and the forest aggregates them, typically by averaging probabilities (soft voting).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2F1%2Ai0o8mjFfCn-uD79-F1Cqkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2F1%2Ai0o8mjFfCn-uD79-F1Cqkw.png" alt="Decision tree and random forest diagram" width="592" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a deeper refresher, the scikit-learn documentation provides a clear overview of the algorithmic details: &lt;a href="https://scikit-learn.org/stable/modules/tree.html" rel="noopener noreferrer"&gt;https://scikit-learn.org/stable/modules/tree.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important part for this discussion is this: inference is just branching logic and basic arithmetic. There is no heavy linear algebra involved.&lt;/p&gt;





&lt;h2 id="training-runtime-vs-inference-runtime"&gt;Training Runtime vs Inference Runtime&lt;/h2&gt;

&lt;p&gt;When we talk about inference cost, we usually think about raw compute: CPU cycles per forward pass, vectorized math, and numerical throughput. For tree-based models, however, the computation itself is small. A decision tree prediction is mostly a sequence of comparisons, and a random forest repeats that process across multiple trees before averaging the results. That means the largest cost often comes from the machinery around the model rather than from the model logic itself: importing large numerical libraries, deserializing pickled objects, allocating arrays, shipping container images with hundreds of megabytes of dependencies, and managing compatibility across Python versions and architectures.&lt;/p&gt;

&lt;p&gt;Those costs make sense in a training environment, where flexibility, experimentation, and rich APIs matter. They make much less sense in an inference environment, where the model has already been trained and the runtime only needs to execute a fixed set of operations. TreeCompiler explores what happens when we separate those concerns. By turning the learned tree structure into explicit source code, inference no longer depends on a general-purpose ML framework to load and execute a serialized artifact. The deployed program contains only the branching logic required for prediction, which reduces startup work, dependency loading, object allocation, and packaging overhead. In compiled targets, it also gives the compiler a simpler program to optimize: deterministic control flow, constant thresholds, direct comparisons, and small functions instead of a dynamic model object interpreted through a training framework.&lt;/p&gt;





&lt;h2 id="what-the-compiler-actually-does"&gt;What the Compiler Actually Does&lt;/h2&gt;

&lt;p&gt;Scikit-learn stores a trained decision tree as arrays: left children, right children, feature indices, thresholds, and class counts per node. Prediction consists of walking those arrays until a leaf is reached.&lt;/p&gt;

&lt;p&gt;TreeCompiler extracts that structure into an intermediate representation and generates explicit source code with nested if/else branches.&lt;/p&gt;

&lt;p&gt;For a small tree, the generated Python looks like this:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The generated code has no imports and no runtime dependencies, only comparisons and simple arithmetic. For a random forest, TreeCompiler generates one function per tree and performs soft voting by averaging the predicted probabilities. Once inference is represented as explicit source code, the deployment shape changes: scikit-learn is no longer required at runtime, and the model is no longer a serialized artifact loaded by an external framework, but part of the executable itself. That shift directly affects latency, memory usage, packaging, and deployment complexity.&lt;/p&gt;




&lt;h2 id="benchmark-setup"&gt;Benchmark Setup&lt;/h2&gt;

&lt;p&gt;To make the impact concrete, I benchmarked three deployment strategies on AWS Lambda, all using the same model:&lt;/p&gt;

&lt;p&gt;RandomForestClassifier, 50 trees, depth 8, 4 classes, 20 features.&lt;/p&gt;

&lt;p&gt;Each function was configured with 128MB of memory, which is the minimum allocation for AWS Lambda.&lt;/p&gt;

&lt;p&gt;Deployment strategies:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;scikit-learn inside a Docker image.&lt;/li&gt;
  &lt;li&gt;Python code (generated source, standard Python runtime).&lt;/li&gt;
  &lt;li&gt;Compiled Go (generated source compiled into a static binary).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Python and Go expose two different optimization surfaces. In Python, the generated tree code is still executed by an interpreter, so the runtime sees the model as bytecode to evaluate step by step. In Go, the generated source is compiled ahead of time into native machine code. That gives the compiler a chance to apply optimizations before the function ever runs: lowering comparisons and arithmetic into direct CPU instructions, choosing efficient register and stack layouts.&lt;/p&gt;

&lt;p&gt;Once inference is reduced to pure branching logic, the runtime itself becomes the dominant factor.&lt;/p&gt;




&lt;h2 id="python-vs-compiled-python-same-language-different-runtime-shape"&gt;Python vs Compiled Python (Same Language, Different Runtime Shape)&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;scikit-learn (Docker)&lt;/th&gt;
      &lt;th&gt;Python Compiled&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Cold init&lt;/td&gt;
      &lt;td&gt;2.05s&lt;/td&gt;
      &lt;td&gt;450.8ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Warm p50&lt;/td&gt;
      &lt;td&gt;22.4ms&lt;/td&gt;
      &lt;td&gt;1.8ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Warm p95&lt;/td&gt;
      &lt;td&gt;30.5ms&lt;/td&gt;
      &lt;td&gt;15.7ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Memory&lt;/td&gt;
      &lt;td&gt;201 MB&lt;/td&gt;
      &lt;td&gt;127 MB&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cold init measures the time required to initialize the execution environment before the first request. Warm p50 and warm p95 measure latency after the environment is already running: p50 captures the median request, while p95 captures tail latency near the slowest 5% of requests. Memory is the peak memory consumption observed during execution. Under those definitions, compiled Python changes the shape of the service even though the model is the same: cold initialization drops from 2.05 seconds to 450.8 milliseconds, roughly &lt;strong&gt;4.5x faster&lt;/strong&gt;, or about a 78% reduction in startup time. Warm p50 falls from 22.4ms to 1.8ms, &lt;strong&gt;over 12x faster&lt;/strong&gt;, roughly a 92% reduction in median latency. Warm p95 drops from 30.5ms to 15.7ms, almost &lt;strong&gt;2x faster&lt;/strong&gt; at the tail. Memory decreases from 201MB to 127MB, a &lt;strong&gt;37% reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those improvements do not come from changing the forest, the features, or the prediction rule. They come from changing what has to exist at inference time. The generated Python function still runs on the Python interpreter, but it no longer needs to import and initialize scikit-learn, load a pickled object graph, allocate NumPy structures, or carry a full scientific stack just to execute a sequence of branches. Even within the same language, replacing a framework-driven runtime with explicit prediction code produces structural gains in startup time, latency, and memory usage.&lt;/p&gt;




&lt;h2 id="compiled-python-vs-compiled-go-different-runtime-model"&gt;Compiled Python vs Compiled Go (Different Runtime Model)&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;Python Compiled&lt;/th&gt;
      &lt;th&gt;Go Compiled&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Cold init&lt;/td&gt;
      &lt;td&gt;450.8ms&lt;/td&gt;
      &lt;td&gt;78.3ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Warm p50&lt;/td&gt;
      &lt;td&gt;1.8ms&lt;/td&gt;
      &lt;td&gt;1.1ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Warm p95&lt;/td&gt;
      &lt;td&gt;15.7ms&lt;/td&gt;
      &lt;td&gt;11.5ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Memory&lt;/td&gt;
      &lt;td&gt;127 MB&lt;/td&gt;
      &lt;td&gt;21 MB&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Moving from pure Python to compiled Go pushes the same generated model into a different execution model. Cold initialization drops from 450.8ms to 78.3ms, roughly &lt;strong&gt;5.7x faster&lt;/strong&gt;, or an 83% reduction. Warm p50 improves from 1.8ms to 1.1ms, about &lt;strong&gt;1.6x faster&lt;/strong&gt;, while warm p95 drops from 15.7ms to 11.5ms, improving tail latency by roughly &lt;strong&gt;27%&lt;/strong&gt;. Memory falls from 127MB to 21MB, an &lt;strong&gt;83% reduction&lt;/strong&gt;, or about &lt;strong&gt;6x smaller&lt;/strong&gt;. The warm latency gains are smaller than the scikit-learn to compiled Python jump because both versions already execute minimal prediction logic. The remaining difference comes from the runtime around that logic.&lt;/p&gt;

&lt;p&gt;In the Python version, the generated code is explicit, but it still runs through the Python interpreter and Python object model. In the Go version, the same tree structure is compiled ahead of time into a static binary, so the executable can start with much less runtime machinery and the compiler can lower the generated comparisons, constants, and function calls into native code. For a workload made almost entirely of deterministic branches and simple arithmetic, that matters: there is little numerical work left to optimize, so startup cost, memory footprint, and runtime dispatch become the dominant factors.&lt;/p&gt;




&lt;h2 id="translating-this-into-economic-impact"&gt;Translating This Into Economic Impact&lt;/h2&gt;

&lt;p&gt;In serverless systems, latency and cost are tied to the same underlying factors: how long the function runs, how much memory is allocated, and how much work has to happen before the first request can be served. AWS Lambda charges per request and per GB-second, so reducing execution time matters, but the configured memory size matters just as much. This is why peak memory is not only an operational metric, but also a cost signal. A function that peaks around 21MB can safely fit inside the 128MB minimum allocation. A function that peaks near 201MB would normally need at least a 256MB allocation to leave a safe margin.&lt;/p&gt;

&lt;p&gt;The cost impact becomes clearer if we turn the benchmark into a concrete traffic scenario. Assume 100 million invocations per month on x86 Lambda, excluding the free tier and excluding surrounding services such as API Gateway, CloudWatch Logs, storage, and networking. The request charge is the same for all implementations, so the interesting part is the duration cost. For the estimate below, the scikit-learn Docker version is modeled at 256MB because of its observed memory usage, while compiled Python and compiled Go are modeled at 128MB. Average billed duration is approximated as:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;warm p50 latency + cold start rate × cold init time
&lt;/code&gt;&lt;/pre&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Cold start rate&lt;/th&gt;
      &lt;th&gt;scikit-learn Docker&lt;/th&gt;
      &lt;th&gt;Compiled Python&lt;/th&gt;
      &lt;th&gt;Compiled Go&lt;/th&gt;
      &lt;th&gt;Go vs scikit-learn&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0%&lt;/td&gt;
      &lt;td&gt;$29.33/month&lt;/td&gt;
      &lt;td&gt;$20.38/month&lt;/td&gt;
      &lt;td&gt;$20.23/month&lt;/td&gt;
      &lt;td&gt;31% lower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1%&lt;/td&gt;
      &lt;td&gt;$37.88/month&lt;/td&gt;
      &lt;td&gt;$21.31/month&lt;/td&gt;
      &lt;td&gt;$20.39/month&lt;/td&gt;
      &lt;td&gt;46% lower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10%&lt;/td&gt;
      &lt;td&gt;$114.75/month&lt;/td&gt;
      &lt;td&gt;$29.77/month&lt;/td&gt;
      &lt;td&gt;$21.86/month&lt;/td&gt;
      &lt;td&gt;81% lower&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table also shows why a latency chart alone does not fully explain the deployment impact. In the warm-dominated case, request charges become the floor, so the total bill cannot fall by 12x even though median latency does. The compute component does fall almost that much: in the 1% cold-start scenario, moving from scikit-learn Docker to compiled Go reduces duration cost from $17.88/month to $0.39/month, a roughly 98% reduction. Under burstier traffic, the difference is larger because cold initialization becomes part of the cost profile. Reducing cold starts from 2.05 seconds to 78.3 milliseconds changes both user-visible latency and billed initialization work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbarufa%2Fbarufa.github.io%2Frefs%2Fheads%2Fmain%2Fassets%2Fimg%2Ftreecompiler_benchmark.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbarufa%2Fbarufa.github.io%2Frefs%2Fheads%2Fmain%2Fassets%2Fimg%2Ftreecompiler_benchmark.png" alt="Benchmark" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The broader point is not that every tree model will save exactly this amount. The point is that deployment shape changes the cost equation. Removing the training stack from the inference path reduces package size, initialization work, runtime memory pressure, and execution overhead. For low-volume workloads, the dollar difference may be small because the request charge and free tier dominate. For high-volume or bursty workloads, the same structural changes can translate into meaningful reductions in billed compute and a lower need to overprovision memory.&lt;/p&gt;





&lt;h2 id="broader-lesson"&gt;Broader Lesson&lt;/h2&gt;

&lt;p&gt;This idea is not limited to trees.&lt;/p&gt;

&lt;p&gt;In a &lt;a href="https://stuckinalocalminima.com/blog/2025/sklearn-faiss" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, I explored a similar separation between experimentation tooling and production inference when migrating PCA projections from scikit-learn to Faiss for scalable vector search. The theme was the same: use rich libraries for training and validation, but deploy inference using the runtime that best matches operational constraints. In that case, benchmarking showed roughly a &lt;strong&gt;1.77× improvement&lt;/strong&gt; in throughput simply by moving the projection step to a runtime designed for high-performance inference.&lt;/p&gt;

&lt;p&gt;Trees simply make that principle easier to visualize because their inference logic is explicit and finite.&lt;/p&gt;

&lt;p&gt;The broader lesson is straightforward.&lt;/p&gt;

&lt;p&gt;Inference is a systems problem as much as it is a modeling problem. The largest optimizations are not just in the math.&amp;nbsp;They are in choosing the right runtime for the job.&lt;/p&gt;





&lt;h2 id="references"&gt;References&lt;/h2&gt;

&lt;p&gt;TreeCompiler repository: &lt;a href="https://github.com/barufa/TreeCompiler" rel="noopener noreferrer"&gt;https://github.com/barufa/TreeCompiler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;scikit-learn DecisionTreeClassifier documentation: &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html" rel="noopener noreferrer"&gt;https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;scikit-learn Tree module overview: &lt;a href="https://scikit-learn.org/stable/modules/tree.html" rel="noopener noreferrer"&gt;https://scikit-learn.org/stable/modules/tree.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related post on separating training and inference runtimes: &lt;a href="https://stuckinalocalminima.com/blog/2025/sklearn-faiss" rel="noopener noreferrer"&gt;https://stuckinalocalminima.com/blog/2025/sklearn-faiss&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>performance</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Amazon S3 Vectors: What It Is, Where It Fits, and the Gotchas Nobody Tells You</title>
      <dc:creator>Bruno Baruffaldi</dc:creator>
      <pubDate>Fri, 05 Dec 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/barufa/amazon-s3-vectors-what-it-is-where-it-fits-and-the-gotchas-nobody-tells-you-57d8</link>
      <guid>https://dev.to/barufa/amazon-s3-vectors-what-it-is-where-it-fits-and-the-gotchas-nobody-tells-you-57d8</guid>
      <description>&lt;p&gt;S3 Vectors adds &lt;strong&gt;native vector storage + ANN search&lt;/strong&gt; to S3 via &lt;em&gt;vector buckets&lt;/em&gt; and &lt;em&gt;vector indexes&lt;/em&gt;. It’s cost‑oriented, elastic, and ideal for big volumes with &lt;strong&gt;sub‑second&lt;/strong&gt; queries when throughput is moderate. It’s in &lt;strong&gt;preview&lt;/strong&gt; and has sharp edges: hard &lt;strong&gt;Top‑K=30&lt;/strong&gt; , only &lt;strong&gt;float32&lt;/strong&gt; vectors. This post focuses on where it fits and the gotchas that actually matter in design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why S3 Vectors matters
&lt;/h2&gt;

&lt;p&gt;Embeddings are everywhere (RAG, recommendations, search). Most teams start with a hosted vector DB and quickly hit two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost at scale&lt;/strong&gt; : millions to billions of vectors add up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops overhead&lt;/strong&gt; : clusters, replicas, upgrades, and capacity planning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;S3 Vectors&lt;/strong&gt; flips that default: store vectors in S3 with a &lt;strong&gt;purpose‑built index&lt;/strong&gt; that you can &lt;strong&gt;query directly&lt;/strong&gt;. You don’t manage nodes; you pay S3‑style pricing for storage + per‑request querying. If your workload tolerates low to mid QPS but needs large capacity and durability, this is a compelling baseline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Concepts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector bucket&lt;/strong&gt; : a special S3 bucket type for vectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector index&lt;/strong&gt; : a logical container inside a vector bucket where you write/query vectors (all vectors in an index share &lt;strong&gt;dimension&lt;/strong&gt; and &lt;strong&gt;distance metric&lt;/strong&gt; ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector&lt;/strong&gt; : &lt;code&gt;{ key, values: float32[d], metadata }&lt;/code&gt;, where metadata can be filterable/non‑filterable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries&lt;/strong&gt; : approximate nearest neighbor ( &lt;strong&gt;ANN&lt;/strong&gt; ) with optional metadata filters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key properties you’ll design around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimension&lt;/strong&gt; : 1…4096 (set at index creation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance&lt;/strong&gt; : Cosine or Euclidean (immutable per index).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt; : write RPS per index is low; design for batching and sharding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve limit&lt;/strong&gt; : Top-K is capped at 30 per similarity query — and there is no pagination mechanism for ANN queries.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hard limits (and how to design around them)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Top‑K ≤ 30&lt;/strong&gt; (no pagination). If you need &amp;gt;30 another vector database could be more suitable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inserts&lt;/strong&gt; : up to &lt;strong&gt;500 vectors&lt;/strong&gt; per &lt;code&gt;PutVectors&lt;/code&gt;; &lt;strong&gt;5 write RPS per index&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data types&lt;/strong&gt; : &lt;strong&gt;&lt;code&gt;float32&lt;/code&gt; only&lt;/strong&gt; for vector values. If you pass other types, S3 Vectors converts to float32.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immutable index schema&lt;/strong&gt; : &lt;code&gt;dimension&lt;/code&gt;, &lt;code&gt;distance&lt;/code&gt;, and &lt;strong&gt;non‑filterable metadata keys&lt;/strong&gt; can’t be changed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Things AWS docs don’t spell out (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFormation / Terraform&lt;/strong&gt; : &lt;strong&gt;no native resources&lt;/strong&gt; to create vector buckets/indexes in preview — automate with &lt;strong&gt;CLI/SDK&lt;/strong&gt; from CDK or CFN &lt;strong&gt;Custom Resources&lt;/strong&gt;. See &lt;a href="https://repost.aws/questions/QUiFFFtn5rTPCi83CC-Tp5-Q/setting-up-s3-vector-buckets-via-terraform" rel="noopener noreferrer"&gt;AWS re:Post confirmation (no CFN/Terraform support as of Sep 2025)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedup across indexes?&lt;/strong&gt; Not documented. Pricing/examples suggest you &lt;strong&gt;pay per copy per index&lt;/strong&gt;. If you want to “reuse” embeddings, prefer &lt;strong&gt;a single index&lt;/strong&gt; with metadata/filters, or partition by namespaces/tenants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bucket with many indexes or many buckets?&lt;/strong&gt; Best practice: &lt;strong&gt;one bucket&lt;/strong&gt; with &lt;strong&gt;multiple indexes&lt;/strong&gt; and access control via IAM/bucket policies. Use many buckets only for strong isolation (accounts/regions/KMS/lifecycle). See &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-best-practices.html" rel="noopener noreferrer"&gt;S3 Vectors best practices&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cost intuition (rule‑of‑thumb)
&lt;/h2&gt;

&lt;p&gt;S3 Vectors aims to &lt;strong&gt;lower total cost&lt;/strong&gt; for large volumes with moderate QPS: you pay S3‑like storage + per‑request costs for index/query. &lt;strong&gt;No cluster provisioning&lt;/strong&gt; , and batch ingest jobs are cheap.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to choose S3 Vectors vs alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;S3 Vectors&lt;/th&gt;
&lt;th&gt;OpenSearch (Serverless/Managed)&lt;/th&gt;
&lt;th&gt;SaaS Vector DB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Massive volume, low cost, low‑to‑mid QPS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maybe (if QPS climbs)&lt;/td&gt;
&lt;td&gt;Maybe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra‑low latency, high QPS, complex filters&lt;/td&gt;
&lt;td&gt;Can fall short&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple ops (no clusters)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less&lt;/td&gt;
&lt;td&gt;Less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock tie‑in (embeddings/KB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Indirect&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  S3 Vectors vs Upstash Vectors (serverless)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;High‑level&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;S3 Vectors&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Upstash Vectors&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nature&lt;/td&gt;
&lt;td&gt;Object store with &lt;strong&gt;native vector indexes&lt;/strong&gt; (preview)&lt;/td&gt;
&lt;td&gt;Managed &lt;strong&gt;serverless vector DB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Storage + per‑request query (see AWS announcement)&lt;/td&gt;
&lt;td&gt;Per‑request pricing (e.g., $0.4/100K req in PAYG) + storage (e.g., $0.25/GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query limits&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Top‑K ≤ 30&lt;/strong&gt; , &lt;strong&gt;no pagination&lt;/strong&gt; in &lt;code&gt;QueryVectors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Client‑set &lt;code&gt;topK&lt;/code&gt; &lt;strong&gt;with pagination&lt;/strong&gt; via &lt;strong&gt;Resumable Query&lt;/strong&gt; (cursor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtering&lt;/td&gt;
&lt;td&gt;Metadata filters; declare &lt;strong&gt;non‑filterable&lt;/strong&gt; keys at index create&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Metadata filtering&lt;/strong&gt; ; &lt;strong&gt;namespaces&lt;/strong&gt; for isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data type&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;float32&lt;/code&gt; only&lt;/strong&gt; for vector values&lt;/td&gt;
&lt;td&gt;Upstash accepts numeric vector payloads (not advertised as multiple dtypes; equivalently treated as float arrays).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi‑tenancy&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One bucket, many indexes&lt;/strong&gt; (index per tenant) recommended&lt;/td&gt;
&lt;td&gt;Multiple &lt;strong&gt;indexes&lt;/strong&gt; + &lt;strong&gt;namespaces&lt;/strong&gt; ; multiple DBs by project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-amazon-s3-vectors-first-cloud-storage-with-native-vector-support-at-scale/" rel="noopener noreferrer"&gt;AWS announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-limitations.html" rel="noopener noreferrer"&gt;S3 Vectors Query limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://upstash.com/pricing/vector" rel="noopener noreferrer"&gt;Upstash pricing&lt;/a&gt; and &lt;a href="https://upstash.com/docs/vector/help/faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://upstash.com/docs/vector/features/filtering" rel="noopener noreferrer"&gt;Upstash filtering &amp;amp; namespaces&lt;/a&gt; and &lt;a href="https://upstash.com/docs/vector/sdks/py/example_calls/query" rel="noopener noreferrer"&gt;SDK query params&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://upstash.com/docs/vector/features/resumablequery" rel="noopener noreferrer"&gt;Upstash Resumable Query&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implications&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you need &lt;strong&gt;scroll/infinite‑results UIs&lt;/strong&gt; , Upstash’s &lt;strong&gt;resumable query&lt;/strong&gt; is simpler. In S3 Vectors you’ll fan‑out across segments and re‑rank to bypass &lt;code&gt;Top‑K=30&lt;/code&gt;. The fan-out could end in a clique with means some vectors would never be retrieved.&lt;/li&gt;
&lt;li&gt;If your profile is &lt;strong&gt;huge corpuses + moderate QPS + lowest storage cost&lt;/strong&gt; , S3 Vectors is attractive. For &lt;strong&gt;real‑time UX&lt;/strong&gt; with richer ranking/pagination, Upstash is frictionless.&lt;/li&gt;
&lt;li&gt;Multi‑tenant: both solve it, but S3 Vectors centralizes observability/logging per bucket; Upstash leans on namespaces and per‑index isolation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;S3 Vectors won’t replace every vector DB —but it &lt;strong&gt;changes the starting point&lt;/strong&gt;. If your priority is &lt;strong&gt;cost and simplicity&lt;/strong&gt; with large datasets and moderate QPS, start with S3 Vectors and scale out to OpenSearch or a dedicated vector DB only where the &lt;strong&gt;latency/query profile&lt;/strong&gt; demands it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions you want me to benchmark next?&lt;/em&gt; Throughput vs. shards, recall vs. filters, or evaluator design for reranking under &lt;code&gt;Top‑K=30&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>cloud</category>
    </item>
    <item>
      <title>From scikit-learn to Faiss: Migrating PCA for Scalable Vector Search</title>
      <dc:creator>Bruno Baruffaldi</dc:creator>
      <pubDate>Sat, 19 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/barufa/from-scikit-learn-to-faiss-migrating-pca-for-scalable-vector-search-43l8</link>
      <guid>https://dev.to/barufa/from-scikit-learn-to-faiss-migrating-pca-for-scalable-vector-search-43l8</guid>
      <description>&lt;h2&gt;
  
  
  Why using Faiss
&lt;/h2&gt;

&lt;p&gt;Faiss is a high‑performance library for vector similarity search and related primitives (clustering, compression, linear transforms like PCA). t scales to millions–billions of vectors on CPU and GPU and it is a much faster implementation of PCA. In practice this reduces memory, latency, and Python overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why migrate PCA to Faiss?
&lt;/h3&gt;

&lt;p&gt;If you’re already using scikit-learn for training, why switch to Faiss for deployment?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Training PCA in sklearn is convenient, but for the deployment implementation is slow.&lt;/li&gt;
&lt;li&gt;Faiss offers faster, more efficient kernels for applying PCA at scale.&lt;/li&gt;
&lt;li&gt;You can migrate a trained sklearn.PCA to a faiss.PCAMatrix without retraining.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Principal Component Analysis
&lt;/h2&gt;

&lt;p&gt;PCA (Principal Component Analysis) is a linear dimensionality reduction technique. It projects data into a lower-dimensional space using the eigenvectors of the covariance matrix. You can check &lt;a href="https://youtu.be/dhK8nbtii6I?si=rEa2z5YDaGERLTfy" rel="noopener noreferrer"&gt;this video&lt;/a&gt; for a detail exaplanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sklearn
&lt;/h3&gt;

&lt;p&gt;We’ll focus on the essential operation of PCA: projecting vectors using &lt;code&gt;transform()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Given :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X as the input data&lt;/li&gt;
&lt;li&gt;skl_pca as the trained PCA object from sklearn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can project X into the PCA-transformed space like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;X_transformed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If whitening was applied during PCA fitting, you’ll also need to scale the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explained_variance_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;min_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;eps&lt;/span&gt;
&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_scale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;min_scale&lt;/span&gt;
&lt;span class="n"&gt;X_transformed&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For reference, see the &lt;a href="https://github.com/scikit-learn/scikit-learn/blob/c5497b7f7/sklearn/decomposition/_base.py#L116" rel="noopener noreferrer"&gt;official implementation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faiss
&lt;/h3&gt;

&lt;p&gt;In Faiss, after training a &lt;code&gt;PCAMatrix&lt;/code&gt;, the transformation looks slightly different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="n"&gt;X_transformed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;A&lt;/code&gt; is the components matrix, and &lt;code&gt;b&lt;/code&gt; is a bias vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from &lt;code&gt;sklearn&lt;/code&gt; to &lt;code&gt;Faiss&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To migrate from a trained &lt;code&gt;sklearn.PCA&lt;/code&gt; model to a &lt;code&gt;faiss.PCAMatrix&lt;/code&gt;, you need to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;A&lt;/code&gt;: the transformed components matrix&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b&lt;/code&gt;: the bias vector to match sklearn’s behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on whether whitening is used:&lt;/p&gt;

&lt;p&gt;Without whitening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;whiten=True&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# escala cada fila de skl_pca.components_
&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explained_variance_&lt;/span&gt;&lt;span class="p"&gt;)[:,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After these definitions we can get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PCA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;Let’s create a small PCA model using the USPS digits dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_openml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.decomposition&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PCA&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_openml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;41082&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_frame&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_X_y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stratify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;skl_pca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PCA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s migrate the trained model to Faiss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sklearn_pca_to_faiss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PCAMatrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;d_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;d_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_components_&lt;/span&gt;

    &lt;span class="c1"&gt;# Build A: rows are components; include whitening if requested
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whiten&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explained_variance_&lt;/span&gt;&lt;span class="p"&gt;)[:,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;components_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;faiss_pca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PCAMatrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# eigen_power handled manually
&lt;/span&gt;    &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy_array_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy_array_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Choose bias so that X @ A^T + b == (X - mean) @ A^T
&lt;/span&gt;    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="c1"&gt;# shape (d_out,)
&lt;/span&gt;    &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy_array_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_trained&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;

&lt;span class="n"&gt;faiss_pca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sklearn_pca_to_faiss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: Use Faiss’s &lt;code&gt;copy_array_to_vector&lt;/code&gt; utility to load arrays into Faiss structures. See &lt;a href="https://github.com/facebookresearch/faiss/blob/514b44fca8542bafe8640adcbf1cccce1900f74c/faiss/python/array_conversions.py#L128" rel="noopener noreferrer"&gt;this file&lt;/a&gt; for implementation details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation
&lt;/h3&gt;

&lt;p&gt;Always validate that the migration preserves results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d_in&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Check over some random vectors
&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_allclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_py&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Check over train vectors
&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_allclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_py&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Check over test vectors
&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_allclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_py&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK: sklearn == faiss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# sklearn
&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skl_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# faiss
&lt;/span&gt;&lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss_pca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_py&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;t3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sklearn.transform: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vec/s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faiss.apply_py : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vec/s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Speedup: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the complete code &lt;a href="https://github.com/barufa/barufa.github.io/blob/main/assets/python/pca_migration.py" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Benchmarking on an &lt;a href="https://www.asus.com/us/laptops/for-home/zenbook/zenbook-14-q407/techspec/" rel="noopener noreferrer"&gt;Asus Zenbook 14&lt;/a&gt; showed throughput rising from 803,290 vec/s to 1,418,897 vec/s — roughly a 1.77× improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating from &lt;code&gt;scikit-learn&lt;/code&gt; to &lt;code&gt;Faiss&lt;/code&gt; for PCA application is a straightforward optimization with real-world impact. You can keep sklearn for training and validation, then deploy the exact same projection using Faiss—boosting inference performance without retraining.&lt;/p&gt;

&lt;p&gt;This method is simple, deterministic, and production-ready. And with just a few lines of code, you bridge the gap between experimentation and scalable deployment.&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>performance</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>DVC + Many Files: A Strategy for Efficient Large Dataset Management</title>
      <dc:creator>Bruno Baruffaldi</dc:creator>
      <pubDate>Sun, 30 Jun 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/barufa/dvc-many-files-a-strategy-for-efficient-large-dataset-management-59h</link>
      <guid>https://dev.to/barufa/dvc-many-files-a-strategy-for-efficient-large-dataset-management-59h</guid>
      <description>&lt;p&gt;Implementing DVC at my workplace had been successful for most tasks, but dealing with a dataset containing 1 million images was tough. Uploading and downloading the dataset took several hours, so we had to use some clever tricks to streamline the workflow. In this post, I’ll share the valuable lessons we learned to help you avoid similar pitfalls.&lt;/p&gt;

&lt;h2 id="context"&gt;Context&lt;/h2&gt;

&lt;p&gt;In an effort to improve data management during experiments, I decided to incorporate DVC. DVC (Data Version Control) is a tool that stores your data in remote storage and simplifies its versioning with Git. It hashes the data and saves the hash along with other metadata in a source-control file that is added to the repository. The actual data is then transferred to a corresponding folder in the remote storage. That way, DVC allows you to version your data by tracking a “pointer” with Git and restore the corresponding files with a single DVC command.&lt;/p&gt;

&lt;p&gt;At first, everything worked smoothly. We could upload and download data without any issues. It was easy to track which version of our code was using which version of the data for running our experiments. However, things got complicated when we tried to work with datasets containing around 1 million images and beyond.&lt;/p&gt;

&lt;p&gt;It turns out that DVC struggles with handling a large number of files (&amp;gt;200k)&lt;a href="https://discuss.dvc.org/t/dvc-with-external-data-is-very-slow/1121" rel="noopener noreferrer"&gt;[1]&lt;/a&gt;&lt;a href="https://github.com/iterative/dvc/issues/7607" rel="noopener noreferrer"&gt;[2]&lt;/a&gt;. In our case, it took over 8 hours to upload the data with DVC and another 3 hours to download it. These times were clearly unmanageable for us. Once the download was complete, DVC needed an additional ~1.5 hours to check out the files. The conclusion was clear: we had a huge performance issue!&lt;/p&gt;

&lt;h2 id="problem"&gt;Problem&lt;/h2&gt;

&lt;p&gt;DVC has to check every file to make sure it’s the right one and avoid uploading the same file twice. This computation takes time and could be inefficiently implemented. On top of the overhead per request, there’s another problem that we ran into. It costs a lot of money to send 1 million requests to the storage account. So, we had to come up with a solution to make DVC work for us.&lt;/p&gt;

&lt;h2 id="solution"&gt;Solution&lt;/h2&gt;

&lt;p&gt;The solution we decided to implement was to zip the images in several archives. That way, we could reduce the number of files DVC had to check. The easiest way to do this was to use the &lt;code class="language-plaintext highlighter-rouge"&gt;zip&lt;/code&gt; command in Linux. We created a script that zipped the images in groups and uploaded them to the remote storage. This way, we reduced the number of files DVC had to check from 1 million to ~1000. This change significantly improved the upload and download times.&lt;/p&gt;

&lt;p&gt;Here is the script we used to zip the images:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;#!/bin/bash

# The directory structure should be as follows:
# imagenet/
# ├── n01440764
# │   ├── n01440764_10026.JPEG
# │   ├── n01440764_10027.JPEG
# │   ........
# ├── n01440765
# │   ├── n01440765_1000.JPEG
# │   ........
# .........

directory_path="imagenet"
max_processes=$(nproc --all)

# Function to handle ZIP operation using parallel
zip_directories() {
    find "$directory_path" -mindepth 1 -maxdepth 1 -type d | \
    parallel --progress -j "$max_processes" 'zip -qr {}.zip {} &amp;amp;&amp;amp; rm -rf {}'
}

# Function to handle UNZIP operation using parallel
unzip_files() {
    find "$directory_path" -type f -name "*.zip" | \
    parallel --progress -j "$max_processes" 'unzip -q {} &amp;amp;&amp;amp; rm {}'
}

# Main script logic
if [ $# -ne 1 ]; then
    echo "Usage: $0 [ZIP|UNZIP]"
    exit 1
fi

action=$1

case $action in
    ZIP)
        zip_directories
        ;;
    UNZIP)
        unzip_files
        ;;
    *)
        echo "Error: Unrecognized option '$action'. Usage: $0 [ZIP|UNZIP]"
        exit 1
        ;;
esac

exit 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This solution has some points to consider:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I strongly recommend grouping the files in a way that makes sense for your project. In our case, we use some metadata (eg. labeling session) that naturally partition the dataset. This way, we could easily navigate through the dataset and find the images we needed without having to download/update the whole dataset.&lt;/li&gt;
  &lt;li&gt;Adding new data to the dataset should not modify the existing archives. This way, DVC will not store the same files twice. The new data should be added to a new archive. This is easy to achieve based on the metadata used to partition the dataset. By using the labeling sessions, you can add new zips without changing the existing ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another solution was proposed at &lt;a href="https://fizzylogic.nl/2023/01/13/did-you-know-dvc-doesn-t-handle-large-datasets-neither-did-we-and-here-s-how-we-fixed-it" rel="noopener noreferrer"&gt;this article&lt;/a&gt;, which uses &lt;code class="language-plaintext highlighter-rouge"&gt;Parquet&lt;/code&gt; for partitioning the data instead of zipping. This clever solution is more efficient for some cases, but it requires more effort to implement and may not apply to CV datasets.&lt;/p&gt;

&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;

&lt;p&gt;DVC is a great tool for managing data in machine learning projects. However, it struggles with large datasets containing a large number of files. To overcome this limitation, we zipped the images in groups and uploaded them to the remote storage. This change significantly improved the upload and download times as it reduced the number of files being tracked. I hope this post helps you avoid similar pitfalls when working with large datasets in DVC. If you have any questions or suggestions, feel free to leave a comment below. I’d love to hear from you!&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>dataengineering</category>
      <category>mlops</category>
      <category>python</category>
    </item>
  </channel>
</rss>
