<?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: Dev Jhawar</title>
    <description>The latest articles on DEV Community by Dev Jhawar (@dev_jhawar).</description>
    <link>https://dev.to/dev_jhawar</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%2F4196527%2F748f1502-a6ff-420d-a2a4-288d3fa36d4c.png</url>
      <title>DEV Community: Dev Jhawar</title>
      <link>https://dev.to/dev_jhawar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev_jhawar"/>
    <language>en</language>
    <item>
      <title>Bigger AI models aren't always better: how a 70B model beat a 280B one by reading more</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 19:02:31 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/bigger-ai-models-arent-always-better-how-a-70b-model-beat-a-280b-one-by-reading-more-2jmh</link>
      <guid>https://dev.to/dev_jhawar/bigger-ai-models-arent-always-better-how-a-70b-model-beat-a-280b-one-by-reading-more-2jmh</guid>
      <description>&lt;p&gt;For a few years, the way to build a better AI model was simple. Make it bigger.&lt;/p&gt;

&lt;p&gt;Then in 2022 DeepMind trained a model called Chinchilla. It had &lt;strong&gt;70 billion&lt;/strong&gt; parameters. It beat their own older model, Gopher, which had &lt;strong&gt;280 billion&lt;/strong&gt;. A quarter of the size, better scores.&lt;/p&gt;

&lt;p&gt;How? Chinchilla didn't have a smarter design. It just read a lot more.&lt;/p&gt;

&lt;p&gt;I wanted to see that for myself, so I trained a pile of tiny models on my laptop. Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, two words
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameters&lt;/strong&gt;: the adjustable numbers inside a model. "70B" means 70 billion of them. Think of it as the size of the model's brain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens&lt;/strong&gt;: the chunks of text a model reads while training. Think of it as how much it studied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Training costs compute, and compute is roughly &lt;em&gt;parameters times tokens&lt;/em&gt;. (Times about 6, if you want the exact napkin version.) That means you can spend a budget two ways: a big brain that studies a little, or a smaller brain that studies a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old advice, and the correction
&lt;/h2&gt;

&lt;p&gt;In 2020 a paper from OpenAI (Kaplan et al.) found that a model's error drops smoothly and predictably as you add parameters, data, or compute. Great, you can plan with that. Their advice for a fixed budget was to go for a very large model, train it on a relatively modest amount of data, and stop before it fully converges.&lt;/p&gt;

&lt;p&gt;Two years later, the Chinchilla paper (Hoffmann et al.) re-ran the question more carefully and came to a different answer: &lt;strong&gt;model size and training tokens should grow together.&lt;/strong&gt; Double the model, double the data.&lt;/p&gt;

&lt;p&gt;To prove it, they trained Chinchilla with about the same compute budget as Gopher, but spent it differently:&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%2Fg6wyq5sydtbwugcgrg37.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%2Fg6wyq5sydtbwugcgrg37.png" alt="Two ways to spend one budget: Gopher, a big model with little reading, against Chinchilla, a smaller model with lots of reading" width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Training tokens&lt;/th&gt;
&lt;th&gt;MMLU score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gopher&lt;/td&gt;
&lt;td&gt;280B&lt;/td&gt;
&lt;td&gt;300B&lt;/td&gt;
&lt;td&gt;60.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinchilla&lt;/td&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;1.4T&lt;/td&gt;
&lt;td&gt;about 67.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fjl5udxx2hk2bhak5l0v8.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%2Fjl5udxx2hk2bhak5l0v8.png" alt="Bar charts comparing Gopher and Chinchilla on model size, training tokens and MMLU score" width="799" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(MMLU is a big multiple-choice knowledge test. The paper's abstract says 67.5%, a table inside says 67.6%. Same story either way.) Chinchilla also beat GPT-3 (175B), Jurassic-1 (178B) and Megatron-Turing NLG (530B) on a wide range of tasks.&lt;/p&gt;

&lt;p&gt;The conclusion in the paper: a lot of the big models of the time were &lt;strong&gt;undertrained&lt;/strong&gt;. Too much brain, not enough studying.&lt;/p&gt;

&lt;p&gt;The rule of thumb people took from it: roughly &lt;strong&gt;20 tokens of training data per parameter&lt;/strong&gt;. Chinchilla itself is 1.4T / 70B, which is 20.&lt;/p&gt;

&lt;h2&gt;
  
  
  A calculator
&lt;/h2&gt;

&lt;p&gt;That rule plus the napkin formula gives you a tiny calculator. If compute is &lt;code&gt;6 × N × D&lt;/code&gt; and &lt;code&gt;D = 20 × N&lt;/code&gt;, then &lt;code&gt;compute = 120 × N²&lt;/code&gt;. Flip it around:&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;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;split_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compute_flops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_param&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&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;compute_flops&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_param&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_param&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1e22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.76e23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e24&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; FLOPs -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1e9&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;B parameters, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1e12&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&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;T tokens&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.00e+22 FLOPs -&amp;gt; 9.1B parameters, 0.18T tokens
5.76e+23 FLOPs -&amp;gt; 69.3B parameters, 1.39T tokens
1.00e+24 FLOPs -&amp;gt; 91.3B parameters, 1.83T tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middle line is the compute number from the Chinchilla paper's table. Out comes 69B parameters and 1.4T tokens. That's Chinchilla, from one line of arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now let me try it on my laptop
&lt;/h2&gt;

&lt;p&gt;Billions of parameters isn't something I can do. But the idea, "for a fixed budget there's a best model size", should show up at any scale. So I made a toy version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; about 12 million characters of Python source code from the standard library, with 5% held out to test on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; small GPT-style transformers working on single characters, from about 3 thousand to 3.2 million parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budgets:&lt;/strong&gt; four fixed compute budgets, from 1e11 to 3e12 FLOPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trick:&lt;/strong&gt; for each budget and each model size, I worked out how many tokens fit in the budget (&lt;code&gt;tokens = compute / (6 × parameters)&lt;/code&gt;) and trained exactly that much. Big model, little data. Small model, lots of data. Same cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;35 training runs. Then I measured the loss on the text the models never saw (lower is better).&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%2Fbq0uep3ns8f6kwkyfwm4.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%2Fbq0uep3ns8f6kwkyfwm4.png" alt="Line chart of held-out loss against model size for four compute budgets, with the best size starred on each line" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each coloured line is one budget. The star is the winner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I saw:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The oversized models lost, badly.&lt;/strong&gt; At the biggest budget, the 3.2-million-parameter model got a loss of 2.61. A model about 150 times smaller, trained on far more text, got 1.75. The big one only got to read about 155 thousand tokens. That's under 0.05 tokens per parameter, a huge brain with one skim of the book.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The winner got bigger as the budget grew.&lt;/strong&gt; Best sizes were about 7.4k, 7.4k, 10.7k and 20.6k parameters, for budgets of 1e11, 3e11, 1e12 and 3e12.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The bottom of each curve is flat.&lt;/strong&gt; At 1e12, models of 7.4k, 10.7k and 20.6k parameters scored 2.068, 2.039 and 2.047. You don't need the exact perfect size. You need to avoid being way too big.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;budget (FLOPs)&lt;/th&gt;
&lt;th&gt;best size (params)&lt;/th&gt;
&lt;th&gt;tokens it trained on&lt;/th&gt;
&lt;th&gt;held-out loss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1e11&lt;/td&gt;
&lt;td&gt;7,408&lt;/td&gt;
&lt;td&gt;2.2M&lt;/td&gt;
&lt;td&gt;2.477&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3e11&lt;/td&gt;
&lt;td&gt;7,408&lt;/td&gt;
&lt;td&gt;6.7M&lt;/td&gt;
&lt;td&gt;2.282&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1e12&lt;/td&gt;
&lt;td&gt;10,688&lt;/td&gt;
&lt;td&gt;15.6M&lt;/td&gt;
&lt;td&gt;2.039&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3e12&lt;/td&gt;
&lt;td&gt;20,640&lt;/td&gt;
&lt;td&gt;24.2M&lt;/td&gt;
&lt;td&gt;1.750&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the same &lt;em&gt;shape&lt;/em&gt; the Chinchilla paper describes: for a fixed budget, too-big loses, and the best model grows as the budget grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am &lt;em&gt;not&lt;/em&gt; claiming
&lt;/h2&gt;

&lt;p&gt;This is a toy, so I want to be straight about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I did not reproduce "20 tokens per parameter".&lt;/strong&gt; My winners trained on hundreds or thousands of tokens per parameter. That's very different from 20. I think several things are mixed in: single-character text is a very different problem from words, my runs are tiny (from a few dozen to about twenty thousand training steps), and the position embeddings count as parameters, which is a big share of a 7k-parameter model. I didn't tune settings per model size either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The biggest budget's winner is at the edge.&lt;/strong&gt; At 3e12, the best model was the smallest one I could afford to test, because even smaller ones would have needed more than about 2.5 passes over my data. So its true best size is probably lower, and I can't tell from this data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some small models read the data more than once.&lt;/strong&gt; For the very smallest ones I let the training text repeat up to about 2.5 times. That's harmless at these sizes, but it isn't the clean "one pass over fresh data" setup in the paper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run per setting.&lt;/strong&gt; No repeats, so small differences could be noise. The big gaps are not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So treat this as "the pattern shows up even in a toy", not "I verified Chinchilla's numbers". For the actual numbers, read the paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you should care even if you never train a model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameter count is a weak signal for quality.&lt;/strong&gt; A smaller model that trained on a lot of data can beat a larger one that didn't. When you pick a model, the question "how much was it trained on?" matters as much as "how big is it?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's the reference point for splitting a training budget.&lt;/strong&gt; "Just make it bigger" stopped being the default answer, and "how much data does this size need?" became a standard question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And then it got stranger.&lt;/strong&gt; Some labs now train small models on far more data than the 20x rule says, on purpose. That's the next post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to play with it, the whole experiment is about 100 lines of PyTorch. Change the data, the budgets or the sizes. I'd love to hear if you see the same curves, or different ones.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>What it actually costs to train an LLM: GPUs, days and dollars, worked out step by step</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:47:59 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/what-it-actually-costs-to-train-an-llm-gpus-days-and-dollars-worked-out-step-by-step-1g5j</link>
      <guid>https://dev.to/dev_jhawar/what-it-actually-costs-to-train-an-llm-gpus-days-and-dollars-worked-out-step-by-step-1g5j</guid>
      <description>&lt;p&gt;You've seen the headlines. "Training a big model costs tens of millions of dollars."&lt;/p&gt;

&lt;p&gt;Okay, but &lt;em&gt;why&lt;/em&gt; that number? Where does it come from? I wanted to count it myself, with a formula short enough to fit on a napkin, and then check the answer against something real.&lt;/p&gt;

&lt;p&gt;Turns out you can get surprisingly close. And the places where you &lt;em&gt;don't&lt;/em&gt; get close are the most interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four words you need
&lt;/h2&gt;

&lt;p&gt;No jargon after this, promise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameters&lt;/strong&gt;: the numbers inside the model that get adjusted during training. "8B" means 8 billion of them. Bigger usually means more capable, and more expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens&lt;/strong&gt;: the little text chunks a model reads. Roughly three quarters of a word each. Training data is counted in tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FLOPs&lt;/strong&gt;: floating point operations, aka individual multiplications and additions. Training is basically an absurd number of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-hour&lt;/strong&gt;: one GPU running for one hour. Rent 1,000 GPUs for 10 hours and you've used 10,000 GPU-hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The napkin formula
&lt;/h2&gt;

&lt;p&gt;Here it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;training FLOPs  ≈  6 × parameters × tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Model size times data size times 6.&lt;/p&gt;

&lt;p&gt;Where does the 6 come from? The usual explanation: for each token, the model does about 2 operations per parameter going forward (making a prediction) and about 4 going backward (figuring out how to improve). 2 + 4 = 6. It's a rule of thumb, not exact, but it's the one everybody uses.&lt;/p&gt;

&lt;p&gt;Quick sanity check, because I don't trust a formula until it matches something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chinchilla, a famous 70B model trained on 1.4 trillion tokens: &lt;code&gt;6 × 70e9 × 1.4e12&lt;/code&gt; = &lt;strong&gt;5.9e23&lt;/strong&gt; FLOPs. The paper's own table says about 5.8e23.&lt;/li&gt;
&lt;li&gt;Llama 3.1 405B, trained on 15.6 trillion tokens: &lt;code&gt;6 × 405e9 × 15.6e12&lt;/code&gt; = &lt;strong&gt;3.8e25&lt;/strong&gt;. Meta's paper says 3.8e25.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Matches. Good enough for a napkin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning FLOPs into hours
&lt;/h2&gt;

&lt;p&gt;Now, how fast does a GPU chew through FLOPs?&lt;/p&gt;

&lt;p&gt;An NVIDIA H100 (the chip these big runs use) has a peak of about &lt;strong&gt;989 TFLOP/s&lt;/strong&gt; in the number format used for training. That's 989 trillion operations per second. Sounds huge. It is.&lt;/p&gt;

&lt;p&gt;But "peak" is the speed on the spec sheet, in a perfect world. Real training spends time moving data around, waiting on other GPUs, and so on. Meta said their best setup reached &lt;strong&gt;over 400 TFLOP/s per GPU&lt;/strong&gt;, which is about 40% of peak. (Their paper puts utilisation at 38 to 43%.)&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPU-hours = FLOPs / (speed per GPU × 3600)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fti3cjoqz7eg4snlnwfc8.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%2Fti3cjoqz7eg4snlnwfc8.png" alt="The napkin formula as five boxes: model size, times data, times 6, divided by GPU speed, gives GPU-hours" width="799" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Napkin vs what Meta reported
&lt;/h2&gt;

&lt;p&gt;Meta published GPU-hours for their models. So I could check my napkin against the real thing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;6 × N × D&lt;/th&gt;
&lt;th&gt;napkin hours at 400 TFLOP/s&lt;/th&gt;
&lt;th&gt;napkin hours at 200 TFLOP/s&lt;/th&gt;
&lt;th&gt;Meta reported&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3 8B&lt;/td&gt;
&lt;td&gt;15T&lt;/td&gt;
&lt;td&gt;7.2e23&lt;/td&gt;
&lt;td&gt;0.5M&lt;/td&gt;
&lt;td&gt;1.0M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3 70B&lt;/td&gt;
&lt;td&gt;15T&lt;/td&gt;
&lt;td&gt;6.3e24&lt;/td&gt;
&lt;td&gt;4.4M&lt;/td&gt;
&lt;td&gt;8.8M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.4M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 405B&lt;/td&gt;
&lt;td&gt;15.6T&lt;/td&gt;
&lt;td&gt;3.8e25&lt;/td&gt;
&lt;td&gt;26.3M&lt;/td&gt;
&lt;td&gt;52.6M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.8M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(I used the nominal sizes, 8B, 70B and 405B. The real counts are a bit different.)&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%2Farxjf8cyb5nqt8jo8pbw.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%2Farxjf8cyb5nqt8jo8pbw.png" alt="Bar charts: napkin GPU-hours versus the hours Meta reported for Llama 3 8B, 70B and 405B, and the share of peak GPU speed reached" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 405B result is fun: Meta reported 30.8M GPU-hours, and my napkin at 400 TFLOP/s said 26.3M. Not bad.&lt;/p&gt;

&lt;p&gt;But look at the 8B. Meta reported &lt;strong&gt;1.3M&lt;/strong&gt; hours, and my napkin said 0.5M. Off by 2.6x.&lt;/p&gt;

&lt;p&gt;So I flipped the question around. If Meta's hours are right, what speed did the GPUs actually deliver? Dividing the work by the reported hours gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;8B: about 154 TFLOP/s, &lt;strong&gt;16%&lt;/strong&gt; of peak&lt;/li&gt;
&lt;li&gt;70B: about 273 TFLOP/s, &lt;strong&gt;28%&lt;/strong&gt; of peak&lt;/li&gt;
&lt;li&gt;405B: about 341 TFLOP/s, &lt;strong&gt;35%&lt;/strong&gt; of peak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bigger the model, the better the GPUs were used. My guess, and it is a guess, is that small models don't keep a huge GPU busy as efficiently, and that the 6ND rule skips some real costs (attention, restarts, evaluation runs). I haven't dug into Meta's logs, so take that as speculation. The lesson I'd actually take: &lt;strong&gt;the napkin is great for order of magnitude, and the weak spot is always GPU speed.&lt;/strong&gt; Use a range, not one number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay, but dollars
&lt;/h2&gt;

&lt;p&gt;GPU-hours times price per GPU-hour.&lt;/p&gt;

&lt;p&gt;Big caveat: Meta owns its GPUs, so this isn't what Meta spent. It's what &lt;em&gt;you'd pay to rent&lt;/em&gt; that much compute, and rental prices vary a lot. I'm using $2 to $4 per H100-hour as an assumption, not a quote.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Meta's reported GPU-hours&lt;/th&gt;
&lt;th&gt;at $2&lt;/th&gt;
&lt;th&gt;at $4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3 8B&lt;/td&gt;
&lt;td&gt;1.3M&lt;/td&gt;
&lt;td&gt;$2.6M&lt;/td&gt;
&lt;td&gt;$5.2M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3 70B&lt;/td&gt;
&lt;td&gt;6.4M&lt;/td&gt;
&lt;td&gt;$12.8M&lt;/td&gt;
&lt;td&gt;$25.6M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 405B&lt;/td&gt;
&lt;td&gt;30.84M&lt;/td&gt;
&lt;td&gt;$61.7M&lt;/td&gt;
&lt;td&gt;$123.4M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's just the final training run, by the way. It doesn't include failed experiments, data work, salaries, or the cost of all the smaller models you train first to figure out what to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  "How many GPUs" is a question about time
&lt;/h2&gt;

&lt;p&gt;Here's the part people miss. GPU-hours is a &lt;em&gt;total&lt;/em&gt;. How many GPUs you need depends on how long you're willing to wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPUs needed = GPU-hours / hours you can wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 8B run at 1.3M GPU-hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On 1,000 GPUs: 1,300 hours, about &lt;strong&gt;54 days&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On 2,048 GPUs: about 635 hours, around &lt;strong&gt;26 days&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Those cluster sizes are my examples, not what Meta used for that model.)&lt;/p&gt;

&lt;p&gt;For the 405B: 30.84M GPU-hours spread over 16,384 GPUs is about 1,880 hours, roughly &lt;strong&gt;78 days&lt;/strong&gt;, if every GPU worked the whole time. Real runs don't, so treat that as a rough ballpark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing as a script. Change the numbers.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tflops_per_gpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dollars_per_gpu_hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_gpus&lt;/span&gt;&lt;span class="o"&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;flops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;
    &lt;span class="n"&gt;gpu_hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flops&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tflops_per_gpu&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1e12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FLOPs&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flops&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPU-hours&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gpu_hours&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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&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;$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gpu_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dollars_per_gpu_hour&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="sh"&gt;"&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;n_gpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gpu_hours&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n_gpus&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;24&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; days on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_gpus&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GPUs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="c1"&gt;# a 1B model, 20 tokens per parameter (Chinchilla's rule of thumb), one H100 at 150 TFLOP/s
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;train_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;20e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;train_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;20e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FLOPs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.20e+20&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GPU-hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;222&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$444&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FLOPs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.20e+20&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GPU-hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;222&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$889&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;So a small 1B model, on a sensible amount of data, is about &lt;strong&gt;$450 to $900&lt;/strong&gt; of rented GPU time. That's a "weekend project and a credit card" number, not a "nation-state" number. It's the 8B going to 70B going to 405B that moves you from "hobby" to "company" because the work multiplies each time.&lt;/p&gt;

&lt;p&gt;And for fun, I measured my own laptop CPU while running small experiments: about 30 GFLOP/s on a tiny model. Same 1B model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.2e20 FLOPs / 3e10 FLOP/s = 4e9 seconds  ≈  127 years
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So no, I won't be training that one on the couch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;6 × parameters × tokens&lt;/code&gt;&lt;/strong&gt; is a good napkin for training work. It reproduced both Chinchilla's and Llama 3.1 405B's published compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU speed is the shaky number.&lt;/strong&gt; Real training reached roughly 16% to 35% of an H100's peak in Meta's reported hours. Always give a range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dollars depend on rent&lt;/strong&gt;, and rent is an assumption. The reported GPU-hours are the solid part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doubling the model, or the data, doubles the bill.&lt;/strong&gt; Doubling both quadruples it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;I used nominal model sizes, rented-GPU prices I picked myself, and Meta's reported hours as ground truth. The explanation for why the 8B ran at a lower share of peak is my guess. And the 6ND rule is a simplification: real runs have extra costs it ignores.&lt;/p&gt;

&lt;p&gt;If you want to check my arithmetic, the numbers in the table come straight from the two Llama model cards and the Llama 3 paper. Run the script with your own model size and see what a "small" model costs. I was surprised how small.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>Why Meta trained a small 8B model on 15 trillion tokens, and why that's cheaper than it sounds</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:47:35 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/why-meta-trained-a-small-8b-model-on-15-trillion-tokens-and-why-thats-cheaper-than-it-sounds-1oep</link>
      <guid>https://dev.to/dev_jhawar/why-meta-trained-a-small-8b-model-on-15-trillion-tokens-and-why-thats-cheaper-than-it-sounds-1oep</guid>
      <description>&lt;p&gt;There's a rule of thumb from a famous paper: for the best result per dollar of training, feed a model about &lt;strong&gt;20 tokens of text for every parameter&lt;/strong&gt; it has.&lt;/p&gt;

&lt;p&gt;An 8-billion-parameter model times 20 is &lt;strong&gt;160 billion tokens&lt;/strong&gt;. Meta's own blog post puts the "Chinchilla-optimal" amount for an 8B model at about 200 billion.&lt;/p&gt;

&lt;p&gt;Llama 3 8B was trained on over &lt;strong&gt;15 trillion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's roughly 75 to 95 times more than the rule says. Either Meta wasted a ridiculous amount of compute, or the rule is answering a different question than the one Meta was asking. I wanted to figure out which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule is about the cost of &lt;em&gt;building&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The Chinchilla paper (2022) asked: if I have a fixed amount of compute to spend on &lt;em&gt;training&lt;/em&gt;, what mix of model size and data gives the best model?&lt;/p&gt;

&lt;p&gt;The answer was to grow both together. Their 70B model, trained on 1.4 trillion tokens, beat the older 280B Gopher, which only saw 300 billion tokens.&lt;/p&gt;

&lt;p&gt;But look at what that question leaves out. A model gets trained &lt;strong&gt;once&lt;/strong&gt;. Then it gets &lt;em&gt;used&lt;/em&gt;. Maybe by millions of people, for years.&lt;/p&gt;

&lt;p&gt;So there are two bills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Building&lt;/strong&gt;: about &lt;code&gt;6 × parameters × tokens&lt;/code&gt; FLOPs, paid once. (FLOPs are just individual multiplications and additions. Training is an absurd number of them.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving&lt;/strong&gt;: about &lt;code&gt;2 × parameters&lt;/code&gt; FLOPs for every token the model processes. Paid again, every time anyone uses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The serving bill scales with model size. A smaller model is cheaper on &lt;em&gt;every single request&lt;/em&gt;, forever.&lt;/p&gt;

&lt;p&gt;So here's the trade. Take a smaller model, train it on way more data than the rule suggests, and you can get a model that's as good as a bigger one. You pay more upfront. You pay less on every answer.&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%2F70vq3mhndlgh0lra7o9f.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%2F70vq3mhndlgh0lra7o9f.png" alt="Two recipe cards: a 26B model trained on 2.3 trillion tokens versus an 8B model trained on 15 trillion tokens, with training and serving costs" width="799" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting numbers on it
&lt;/h2&gt;

&lt;p&gt;The Chinchilla paper also gives a formula that &lt;em&gt;predicts&lt;/em&gt; how good a model will be (lower loss = better) from its size and training data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loss = 1.69 + 406.4 / N^0.34 + 410.7 / D^0.28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;N is parameters, D is tokens. In plain words: there's a floor you can't beat (1.69), then a penalty for having a small model, then a penalty for not having read enough. Make N bigger and the second penalty shrinks. Feed it more data and the third one does.&lt;/p&gt;

&lt;p&gt;Let's use it. First, what does it predict for the 8B on 15T tokens?&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;E&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;B&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;406.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;410.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.28&lt;/span&gt;
&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&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;N&lt;/span&gt;&lt;span class="o"&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;B&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15e12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;# 8B, trained on 15T tokens
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;8e9&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# 8B, at the 20-tokens-per-parameter rule
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.949
2.164
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So by the formula, training the same 8B model on 15T tokens instead of 160B drops the predicted loss from 2.164 to 1.949. That's the whole reason to overtrain: the model keeps getting better, just with diminishing returns. (Meta's blog says both Llama 3 models "continued to improve log-linearly" up to 15T tokens, so that matches the idea.)&lt;/p&gt;

&lt;p&gt;Now flip it around. What's the &lt;em&gt;cheapest-to-train&lt;/em&gt; model that reaches that same predicted loss of 1.949?&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="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15e12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tokens_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&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;N&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
    &lt;span class="nf"&gt;return &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="n"&gt;gap&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&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;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&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;inf&lt;/span&gt;

&lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&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="nf"&gt;logspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;tokens_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&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="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;tokens_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;26.1  2.31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;26B model trained on 2.3 trillion tokens&lt;/strong&gt; is predicted to land in the same place. Its training bill, by the napkin formula, is &lt;code&gt;6 × 26.1e9 × 2.31e12&lt;/code&gt; = &lt;strong&gt;3.6e23 FLOPs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The 8B on 15T costs &lt;code&gt;6 × 8e9 × 15e12&lt;/code&gt; = &lt;strong&gt;7.2e23&lt;/strong&gt;. Twice as much to build.&lt;/p&gt;

&lt;p&gt;So overtraining really is the more expensive way to &lt;em&gt;build&lt;/em&gt;. If building were the whole story, Chinchilla's rule wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  But then people use it
&lt;/h2&gt;

&lt;p&gt;Total cost = building + serving. Serving costs &lt;code&gt;2 × N&lt;/code&gt; per token processed:&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;C_small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N_small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;7.2e23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8e9&lt;/span&gt;
&lt;span class="n"&gt;C_big&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;N_big&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.62e23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;26.1e9&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;served&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1e12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e15&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;C_small&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;N_small&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;served&lt;/span&gt;
    &lt;span class="n"&gt;big&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;C_big&lt;/span&gt;   &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;N_big&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;served&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;served&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;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens served: big model costs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;big&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&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 the small one&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1e+12 tokens served: big model costs 0.56x the small one
1e+13 tokens served: big model costs 1.00x the small one
1e+14 tokens served: big model costs 2.41x the small one
1e+15 tokens served: big model costs 3.14x the small one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F8e74knoe1qyg8fe08l3b.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%2F8e74knoe1qyg8fe08l3b.png" alt="Line chart of total FLOPs against tokens served for the 8B and 26B models, crossing at about 10 trillion tokens" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a break-even point at around &lt;strong&gt;10 trillion tokens served&lt;/strong&gt;. Below that, the bigger, cheaper-to-train model wins. Above it, the small overtrained one does, and the gap keeps widening: at 100 trillion tokens, the big one costs about 2.4 times as much in total.&lt;/p&gt;

&lt;p&gt;For something that's going to be downloaded, fine-tuned and run by a lot of people, you'd expect to be well past that. That's the bargain: &lt;strong&gt;pay extra once, save on every answer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you build agents
&lt;/h2&gt;

&lt;p&gt;Agents are token-hungry. Every step re-reads a long prompt, so you process a lot more tokens than the user ever sees, which pushes you toward the "serving matters" side of that chart.&lt;/p&gt;

&lt;p&gt;It's also a hint about why smaller models keep getting good enough for tool-calling work. A small model that was trained on a mountain of data can behave like a bigger, older model. If you're picking a model for a loop that runs thousands of times a day, size is a cost you pay &lt;em&gt;every call&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;This is a fun calculation, not a proof, so a few things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I'm using a formula far outside its comfort zone.&lt;/strong&gt; The Chinchilla loss formula was fitted on training runs much smaller than 15 trillion tokens. At least one later paper refitted the same data and got quite different constants. Treat the 26B as "somewhere in that ballpark", not an exact answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss isn't quality.&lt;/strong&gt; Two models with the same predicted loss can still differ on real tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FLOPs aren't dollars.&lt;/strong&gt; Serving a big model also needs more GPUs just to fit it in memory, which my &lt;code&gt;2N&lt;/code&gt; shortcut ignores. That would tilt things further toward the small model, but I haven't modelled it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I don't know Meta's actual reasoning.&lt;/strong&gt; I'm not claiming this is why they did it. I'm showing that the numbers make it a reasonable bet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I used the nominal 8B.&lt;/strong&gt; The real parameter count is slightly different.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the snippets, change the model sizes, change how much you expect to serve. The break-even point moves a lot, and that's kind of the point: "optimal" depends on what you're optimising for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>The KV cache is eating your GPU. MQA and GQA are how models fight back</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:16:12 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/the-kv-cache-is-eating-your-gpu-mqa-and-gqa-are-how-models-fight-back-3809</link>
      <guid>https://dev.to/dev_jhawar/the-kv-cache-is-eating-your-gpu-mqa-and-gqa-are-how-models-fight-back-3809</guid>
      <description>&lt;p&gt;Quick recap from the last post: the KV cache saves you from recomputing old tokens, and it's the reason generation is fast.&lt;/p&gt;

&lt;p&gt;But it has a bill. And the bill is memory.&lt;/p&gt;

&lt;p&gt;I wanted to put an actual number on it, because "the cache is big" doesn't mean much until you see the GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;Per token, you store a Key and a Value for every layer and every head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes per token = 2 x layers x kv_heads x head_dim x bytes_per_number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2 is K and V. fp16 is 2 bytes per number. Here it is as code:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kv_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;kv_heads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now plug in a Llama-2 7B style model: 32 layers, 32 heads, head dim 128.&lt;/p&gt;

&lt;p&gt;That's 512 KB for &lt;em&gt;every single token&lt;/em&gt;. At 8k context, 4 GB. At 128k context, &lt;strong&gt;64 GB&lt;/strong&gt;. For one request. Weights not included.&lt;/p&gt;

&lt;p&gt;If you're serving many users at once, each one has their own cache. That's the thing that actually fills your GPU, not the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at the formula again
&lt;/h2&gt;

&lt;p&gt;Layers and head_dim are baked into how good the model is. Bytes per number you can shrink with quantization, but that's a different post.&lt;/p&gt;

&lt;p&gt;The interesting variable is &lt;strong&gt;kv_heads&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In standard multi-head attention (MHA), every query head has its &lt;em&gt;own&lt;/em&gt; key head and value head. 32 query heads, 32 KV heads.&lt;/p&gt;

&lt;p&gt;But do the heads really need separate Keys and Values? Turns out, mostly no.&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%2Fggywonjb5rsrnzci6g9u.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%2Fggywonjb5rsrnzci6g9u.png" alt="Diagram: MHA, GQA and MQA compared, showing how many query heads share each key and value head" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  MQA: one KV head for everybody
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-Query Attention&lt;/strong&gt; keeps all the query heads but shares a &lt;em&gt;single&lt;/em&gt; K head and V head across all of them. 32 query heads, 1 KV head.&lt;/p&gt;

&lt;p&gt;Memory goes down by 32x. The catch is quality: forcing every head to read from one shared set of keys and values can hurt, and training gets a bit touchier.&lt;/p&gt;

&lt;h2&gt;
  
  
  GQA: the compromise
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Grouped-Query Attention&lt;/strong&gt; sits in between. Split the query heads into groups, and each group shares one KV head.&lt;/p&gt;

&lt;p&gt;Llama-3 8B has 32 query heads but only &lt;strong&gt;8 KV heads&lt;/strong&gt;. So 4 query heads share each K/V pair. That's a 4x cut in cache size, and the published results say quality stays close to full MHA.&lt;/p&gt;

&lt;p&gt;Same model shape, three attention styles:&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%2Fsm6sjg551mto40c18p4y.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%2Fsm6sjg551mto40c18p4y.png" alt="Chart: KV cache size in GB at 4k to 128k context for MHA, GQA and MQA" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;KV heads&lt;/th&gt;
&lt;th&gt;KB per token&lt;/th&gt;
&lt;th&gt;8k context&lt;/th&gt;
&lt;th&gt;128k context&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MHA (Llama-2 7B)&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GQA (Llama-3 8B)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MQA (hypothetical, same shape)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.12 GB&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MQA row is me taking the same shape with one KV head, to show the extreme. It's not a model I measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes in the code
&lt;/h2&gt;

&lt;p&gt;Less than you'd think. The only real difference is that you repeat the KV heads to line up with the query heads at attention time:&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;# k, v: (kv_heads, t, head_dim)   q: (q_heads, 1, head_dim)
&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q_heads&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;kv_heads&lt;/span&gt;
&lt;span class="n"&gt;k&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;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# now (q_heads, t, head_dim)
&lt;/span&gt;&lt;span class="n"&gt;v&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;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important detail: you &lt;em&gt;store&lt;/em&gt; the small version in the cache, and expand it only when computing. That's where the memory saving lives. (Real implementations avoid the copy, but the idea is the same.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters if you're building agents
&lt;/h2&gt;

&lt;p&gt;You probably aren't training a model. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a provider says a model supports 128k or 1M context, GQA-style tricks are a big part of why that's affordable to serve at all.&lt;/li&gt;
&lt;li&gt;Long contexts are cheaper per token on models built this way, which shows up in latency and sometimes in price.&lt;/li&gt;
&lt;li&gt;If you self-host, KV memory is &lt;em&gt;the&lt;/em&gt; number that decides how many concurrent users fit on one GPU. Do this arithmetic before buying hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where i'd be careful
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Those numbers are for one sequence at fp16. Batching multiplies them, quantizing the cache divides them.&lt;/li&gt;
&lt;li&gt;"Little quality loss" comes from papers and model cards. I haven't run my own quality evals here, and you shouldn't take it from a blog post either. Check the benchmarks for the specific model you use.&lt;/li&gt;
&lt;li&gt;The GB figures are straight arithmetic from the formula, not profiler output. Real servers add overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: why the same sentence can cost very different amounts depending on the tokenizer. Slightly less hardware, more wallet.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Tokenization is why your agent bill looks weird</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:15:47 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/tokenization-is-why-your-agent-bill-looks-weird-4n47</link>
      <guid>https://dev.to/dev_jhawar/tokenization-is-why-your-agent-bill-looks-weird-4n47</guid>
      <description>&lt;p&gt;Models don't read text. They read integers.&lt;/p&gt;

&lt;p&gt;Between your string and the model there's a tokenizer, and it quietly decides how much you pay, how much fits in the context window, and why your model is weirdly bad at counting letters.&lt;/p&gt;

&lt;p&gt;I always treated it as a preprocessing detail. Then i started counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a tokenizer actually does
&lt;/h2&gt;

&lt;p&gt;It chops text into pieces from a fixed vocabulary, usually 30k to 200k entries, and maps each piece to an id. Common words become one token. Rare stuff gets split into smaller bits.&lt;/p&gt;

&lt;p&gt;You can see it directly with &lt;code&gt;tiktoken&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;
&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cl100k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokenization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; tokenization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pneumonoultramicroscopicsilicovolcanoconiosis&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="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'tokenization'  ['token', 'ization']
' tokenization' [' token', 'ization']
'Pneumonoultramicroscopicsilicovolcanoconiosis'
  ['P', 'ne', 'um', 'on', 'oul', 'tram', 'icro', 'sc', 'op', 'ics', 'il', 'ic', 'ov', 'ol', 'cano', 'con', 'iosis']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common word: two pieces. A rare one: seventeen. Also notice that a leading space changes the token. Small thing, but it's why prompts with different whitespace can behave differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the vocabulary gets built
&lt;/h2&gt;

&lt;p&gt;Most modern ones are some flavour of &lt;strong&gt;BPE&lt;/strong&gt; (byte pair encoding). The idea is almost boringly simple: start with single characters, find the most frequent adjacent pair, merge it into a new symbol, repeat.&lt;/p&gt;

&lt;p&gt;I ran it on a tiny made-up corpus (&lt;code&gt;low&lt;/code&gt; x5, &lt;code&gt;lower&lt;/code&gt; x2, &lt;code&gt;newest&lt;/code&gt; x6, &lt;code&gt;widest&lt;/code&gt; x3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  merge s+t   (9x)   n e w e st
2  merge e+st  (9x)   n e w est
3  merge o+w   (7x)   l ow
4  merge l+ow  (7x)   low
5  merge w+est (6x)   n e west
6  merge n+e   (6x)   ne west
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fahoofvhrz91re3a64kc7.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%2Fahoofvhrz91re3a64kc7.png" alt="Diagram: six toy BPE merge steps on the words low, lower, newest and widest" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do that for tens of thousands of merges over a huge corpus and you get a vocabulary. Whatever was common in the training text gets short, cheap tokens. Whatever wasn't, doesn't.&lt;/p&gt;

&lt;p&gt;That last sentence is the whole post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The families, quickly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BPE&lt;/strong&gt;: merge frequent pairs. GPT-2 style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte-level BPE&lt;/strong&gt;: BPE over raw bytes, so there's never an "unknown" character. Anything can be encoded, it just might take many tokens. GPT-2 and the OpenAI tiktoken encodings work this way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPiece&lt;/strong&gt;: similar, but picks merges by likelihood instead of raw frequency. BERT uses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SentencePiece&lt;/strong&gt;: works on raw text including spaces (no pre-splitting on whitespace), so it handles languages without spaces. T5 and many other models use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So I counted
&lt;/h2&gt;

&lt;p&gt;Same strings, four tokenizers: GPT-2, &lt;code&gt;cl100k_base&lt;/code&gt; (GPT-4 era), &lt;code&gt;o200k_base&lt;/code&gt; (GPT-4o era) and T5's SentencePiece. Normalised to tokens per 100 characters so short and long samples compare.&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%2Fy0jgr9fqz7fm7chl3fni.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%2Fy0jgr9fqz7fm7chl3fni.png" alt="Chart: tokens per 100 characters across GPT-2, cl100k, o200k and T5 for six kinds of text" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Raw counts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Text&lt;/th&gt;
&lt;th&gt;chars&lt;/th&gt;
&lt;th&gt;GPT-2&lt;/th&gt;
&lt;th&gt;cl100k&lt;/th&gt;
&lt;th&gt;o200k&lt;/th&gt;
&lt;th&gt;T5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English sentence&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python snippet&lt;/td&gt;
&lt;td&gt;176&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON tool schema&lt;/td&gt;
&lt;td&gt;610&lt;/td&gt;
&lt;td&gt;329&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;161&lt;/td&gt;
&lt;td&gt;183&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Hindi sentence&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emoji string&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(These are single samples I picked, not a benchmark. Your text will differ. T5 and the others also have different vocabularies, so it isn't a pure "which is better" ranking.)&lt;/p&gt;

&lt;p&gt;A few things jumped out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;English is the happy path.&lt;/strong&gt; Everyone's about the same, because every vocabulary was built mostly on English.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code and JSON halved&lt;/strong&gt; between GPT-2 and the newer encodings. My guess is that newer vocabularies were trained on a lot more code, so runs of whitespace and common JSON punctuation get merged into single tokens. I didn't dig into the vocab files to confirm that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hindi is where it hurts.&lt;/strong&gt; 111 tokens in GPT-2, 78 in cl100k, and 24 in o200k. Same sentence. With the older encoding it's more than 4x the cost and 4x the context used, and I'd guess quality is worse too, though I didn't measure that. Dropping to individual characters is what a byte-level tokenizer does when it has never seen your script much:&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;मेरा&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;म&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;े&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;र&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ा&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# one token per character/mark
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Emoji&lt;/strong&gt; are all over the place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for agents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool schemas are paid for on every call.&lt;/strong&gt; My little 610 character JSON schema is ~160 tokens. Twenty tools like that is 3,000+ tokens of overhead on every single step of the loop. Shorter descriptions, fewer tools per call, and a stable ordering (so prompt caching works, see the KV cache post) all help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pretty-printed JSON costs more than compact JSON.&lt;/strong&gt; Indentation is tokens. Try &lt;code&gt;json.dumps(obj, separators=(",", ":"))&lt;/code&gt; on your tool results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count before you guess.&lt;/strong&gt; Use the actual tokenizer for your model. Characters / 4 is a fine rule of thumb for English and a terrible one for Hindi, code, or emoji.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-English users pay more, in money and in context.&lt;/strong&gt; If your product serves them, measure with their text, not yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weird model failures&lt;/strong&gt; like counting letters in "strawberry" or arithmetic on long numbers are partly tokenization: the model never saw the letters, only the chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;I tested one sample per category, with four tokenizers. It's an illustration of the effect, not a study. Also I'm only showing token &lt;em&gt;counts&lt;/em&gt;; whether a model does well in a language depends on training data, not only on how its text is split.&lt;/p&gt;

&lt;p&gt;If you want to try it: swap in your own strings, your own language, your own tool schemas. Ten minutes, and you will probably find something that surprises you. I did.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Why your agent gets slower and pricier with every tool call</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:09:05 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/why-your-agent-gets-slower-and-pricier-with-every-tool-call-1l6n</link>
      <guid>https://dev.to/dev_jhawar/why-your-agent-gets-slower-and-pricier-with-every-tool-call-1l6n</guid>
      <description>&lt;p&gt;Okay so here's something that bugged me for a while.&lt;/p&gt;

&lt;p&gt;You build an agent. Step 1 feels snappy. By step 15, after a few tool calls and a pile of JSON results, it's noticeably slower, and the bill is climbing in a way that doesn't feel linear.&lt;/p&gt;

&lt;p&gt;I knew the vague answer ("the context is longer"). I didn't really &lt;em&gt;get&lt;/em&gt; it. So i stopped reading and wrote the thing myself, in plain NumPy, to see where the time actually goes.&lt;/p&gt;

&lt;p&gt;Short version: the culprit is a boring little thing called the &lt;strong&gt;KV cache&lt;/strong&gt;, and once you see it, a lot of agent design advice suddenly makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a model writes one token
&lt;/h2&gt;

&lt;p&gt;A transformer generates text one token at a time. For every new token, attention needs the &lt;strong&gt;Key&lt;/strong&gt; and &lt;strong&gt;Value&lt;/strong&gt; vectors of &lt;em&gt;every token before it&lt;/em&gt;. That's how the new token "looks back".&lt;/p&gt;

&lt;p&gt;Here's the catch. Those old Keys and Values don't change. Token 12's Key is the same whether you're generating token 13 or token 913.&lt;/p&gt;

&lt;p&gt;So a naive loop does something silly: at every step it recomputes K and V for the whole sequence, from scratch, then throws them away. Then does it again next step.&lt;/p&gt;

&lt;p&gt;The KV cache is just... not doing that. You compute K and V for a token once, keep them in memory, and append.&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%2F4f0xkchkvti6zczr7xhv.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%2F4f0xkchkvti6zczr7xhv.png" alt="Diagram: without a KV cache every token is reprocessed each step, with a cache only the new token is" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let me show you, not tell you
&lt;/h2&gt;

&lt;p&gt;Here's the toy model. 4 layers, 8 heads, 512 wide, random weights. It's not smart. It doesn't need to be, we're timing the machinery.&lt;/p&gt;

&lt;p&gt;No cache, the full thing re-runs every step:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward_full&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&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;h&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="n"&gt;mask&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;triu&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="nf"&gt;full&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1e9&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qkv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&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="mi"&gt;2&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="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;HD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a cache, only the newest token goes through the projections:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward_cached&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;cache&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&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;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;transpose&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="mi"&gt;2&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="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;HD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same math. Before timing anything I checked both versions produce the same output over 40 steps (max difference around 1e-6). Honestly this was the step i almost skipped, and my first version of the benchmark &lt;em&gt;failed&lt;/em&gt; it, the "no cache" path had a bug and I was about to publish a fake speedup. Check your baselines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;I generated 1,024 tokens each way on my laptop CPU.&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%2Fw4s9ytmb317114pmxtun.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%2Fw4s9ytmb317114pmxtun.png" alt="Chart: time to generate each token with and without a KV cache" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without the cache, the time per token keeps climbing: about 12 ms at step 16, 174 ms at step 512, &lt;strong&gt;550 ms&lt;/strong&gt; at step 1023. With the cache it stays low, 3 ms early on and about 16 ms at the end.&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%2Fnaftb3x4ya7nrt226pe4.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%2Fnaftb3x4ya7nrt226pe4.png" alt="Chart: total time for 1,024 tokens, no cache vs KV cache" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Total time for all 1,024 tokens: &lt;strong&gt;232 seconds vs 12.5 seconds&lt;/strong&gt;. Roughly 18x.&lt;/p&gt;

&lt;p&gt;Two things worth being honest about.&lt;/p&gt;

&lt;p&gt;First, this is a toy on a CPU in NumPy. A real LLM on a GPU has different constants, and nobody should quote my 18x as if it applies to GPT-anything. The &lt;em&gt;shape&lt;/em&gt; is what matters: one curve grows with length, the other mostly doesn't.&lt;/p&gt;

&lt;p&gt;Second, the cached line isn't flat. It still creeps up (3 ms to 16 ms). That's because the new token still has to attend over everything in the cache. The cache removes the &lt;em&gt;recomputation&lt;/em&gt;, not the &lt;em&gt;reading&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay but what about agents
&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about.&lt;/p&gt;

&lt;p&gt;An agent loop looks like this: model call, tool call, append the result, model call again. Every iteration sends the whole conversation back, system prompt, tool schemas, history, all of it.&lt;/p&gt;

&lt;p&gt;If the provider has to process that whole prefix from scratch each time, you pay for it each time, in latency and in input tokens. That's the "slower and pricier with every tool call" feeling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching&lt;/strong&gt; is the same idea one level up. Providers keep the KV cache for a prefix they've seen, so if your next request starts with the same prefix, they skip that work, and usually bill it at a discount.&lt;/p&gt;

&lt;p&gt;Which has some very practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the prefix stable.&lt;/strong&gt; A timestamp or a request ID at the top of your system prompt changes the prefix every call and kills the cache. Put the volatile stuff at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't reorder or edit earlier messages.&lt;/strong&gt; Rewriting history, "cleaning up" older tool results, shuffling tool definitions: all of it breaks the cached prefix from that point onward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append-only is your friend.&lt;/strong&gt; New content goes on the end. That's the pattern caches are built for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big tool results cost you forever.&lt;/strong&gt; A 20k-token JSON dump stays in the context for every following step. Trim it before it goes in, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your provider's rules.&lt;/strong&gt; Cache lifetimes, minimum prefix sizes and pricing differ. Read the docs, don't assume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not glamorous. But it's the difference between an agent that costs cents and one that costs dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: the cache itself is huge
&lt;/h2&gt;

&lt;p&gt;Fine, so just cache everything? The cache lives in memory, and it grows with every token.&lt;/p&gt;

&lt;p&gt;The formula is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes per token = 2 (K and V) x layers x kv_heads x head_dim x bytes per number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Llama-2 7B style model that's &lt;strong&gt;512 KB per token&lt;/strong&gt;. At 128k context, one single request needs about 64 GB just for its cache. That's before the model weights.&lt;/p&gt;

&lt;p&gt;That's why the next thing people ask is "how do we make this smaller?" and the answer is a neat trick with attention heads. I'll cover it in the next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What i'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The KV cache isn't an optimization you opt into. It's why generation is usable at all.&lt;/li&gt;
&lt;li&gt;Your agent gets slower with every step because the context it re-reads keeps growing, and cost follows.&lt;/li&gt;
&lt;li&gt;The cheapest fix is usually boring: stable prefix, append-only history, trimmed tool results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at it yourself, the whole benchmark is about 80 lines. Change the layer count or the length and watch the curves move. Tell me if you get different shapes, i'm curious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Running 100 agents on a free tier (Groq 30 RPM, Tavily credits)</title>
      <dc:creator>Dev Jhawar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:27:27 +0000</pubDate>
      <link>https://dev.to/dev_jhawar/running-100-agents-on-a-free-tier-groq-30-rpm-tavily-credits-pgi</link>
      <guid>https://dev.to/dev_jhawar/running-100-agents-on-a-free-tier-groq-30-rpm-tavily-credits-pgi</guid>
      <description>&lt;p&gt;GridMind is a spreadsheet where every cell is its own research agent. You type "Stripe" in a row, "who is the CEO?" in a column, hit Run, and an agent goes off, searches the web, and fills the cell with an answer and sources.&lt;/p&gt;

&lt;p&gt;You can try it live at &lt;a href="https://grid-mind-tan.vercel.app" rel="noopener noreferrer"&gt;grid-mind-tan.vercel.app&lt;/a&gt;, and the code is on &lt;a href="https://github.com/DEVJHAWAR11/GridMind" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cute idea. Then you do the maths.&lt;/p&gt;

&lt;p&gt;A 10 x 10 sheet is 100 cells. Each cell needs an LLM call to plan, one or two searches, and an LLM call to answer. Groq's free tier gives me around 30 requests a minute. Tavily gives 1000 search credits a month. So one enthusiastic click on "Run all" can eat a decent chunk of the month, and the 429s start before the first row is even done.&lt;/p&gt;

&lt;p&gt;I didn't want to pay for anything yet. So the job became: make the free tier last. It turned out to be four small things, none of them clever.&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%2Fquh60ow24ji8qzo5ys3p.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%2Fquh60ow24ji8qzo5ys3p.png" alt="A filled GridMind sheet, one agent per cell" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A limiter that sits &lt;em&gt;under&lt;/em&gt; the limit
&lt;/h2&gt;

&lt;p&gt;The first fix is the obvious one: don't send more than the provider allows. It's a sliding window, about 20 lines (&lt;a href="https://github.com/DEVJHAWAR11/GridMind/blob/main/backend/app/resilience.py" rel="noopener noreferrer"&gt;&lt;code&gt;resilience.py&lt;/code&gt;&lt;/a&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&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;rate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;now&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;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&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;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;
                &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hits&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details I care about. The limit is set to 25 for Groq, not 30. If you aim exactly at the line, clock skew and a retry or two will push you over it. And the little random sleep at the end is there so a hundred waiting workers don't all wake up on the same tick and stampede.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. When it still fails, retry like an adult
&lt;/h2&gt;

&lt;p&gt;Limiter or not, you will get a 429 or a 503 sometimes. The retry is exponential backoff with jitter, and it respects &lt;code&gt;Retry-After&lt;/code&gt; when the server sends one. If the server tells you how long to wait, believe it:&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;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&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;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&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;retry_after&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&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;uniform&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="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only transient errors (408, 429, 5xx, timeouts) are retried. Hitting my own daily call budget for the LLM is &lt;em&gt;not&lt;/em&gt; transient either, retrying it just burns time, so that one raises a separate non-retryable error.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stop asking the same question twice
&lt;/h2&gt;

&lt;p&gt;This one saved the most. In a sheet about companies, the cells in one row ask near-identical things about the same entity. Searching "Stripe CEO" and "Stripe chief executive" is wasted money when the results are basically the same.&lt;/p&gt;

&lt;p&gt;So searches and page reads go through a small cache with a TTL (an hour by default). The part that matters is the in-flight de-dup: if ten cells ask for the same thing at the same moment, only the first one actually goes out, and the rest wait on its result.&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inflight&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain cache doesn't help here. When you hit Run all, all the identical requests start &lt;em&gt;before any of them has finished&lt;/em&gt;, so there's nothing in the cache yet. Sharing the pending call is what fixes that. If the shared call fails, the failure is shared too, and nothing bad gets cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Spend the paid credit once, and have a floor to fall to
&lt;/h2&gt;

&lt;p&gt;Tavily is good, so it gets the first query of each cell. The second query goes to DuckDuckGo, which is free. A cell costs one credit instead of two.&lt;/p&gt;

&lt;p&gt;On top of that there's a counter that stops me at 900 credits a month, so I never hit the 1000 wall mid-run. And if Tavily errors out or the budget is gone, the search quietly falls back to DuckDuckGo. Results get a bit worse, but the sheet still fills.&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%2Fuv0e7hdqvsqwmde9z7sl.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%2Fuv0e7hdqvsqwmde9z7sl.png" alt="The path one search takes before it costs anything" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DuckDuckGo gets rate-limited a lot too. For the &lt;em&gt;secondary&lt;/em&gt; query that's fine, a miss just returns an empty list and the agent works with what it has. For the primary query, a miss is a real error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;p&gt;Being honest here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every limiter and counter lives &lt;strong&gt;in memory, per instance&lt;/strong&gt;. Run two API instances and you've doubled your limits without meaning to. Going global means Redis counters, which I haven't done yet.&lt;/li&gt;
&lt;li&gt;The fallback means answer quality is not constant. A cell answered from DuckDuckGo snippets can be thinner than one answered from Tavily.&lt;/li&gt;
&lt;li&gt;I picked 25, 40 and 900 by feel and the providers' published limits, not by load testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is fancy. A limiter, a retry, a cache that shares in-flight work, and a budget with a fallback. But together they're why a sheet of 100 agents can run on a setup that costs me nothing.&lt;/p&gt;

&lt;p&gt;If you want to see the limiter at work, open any cell in the live demo and check its trace. Every search the agent made is listed there.&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%2Fj772fqgts1voriiaihlf.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%2Fj772fqgts1voriiaihlf.png" alt="The trace drawer: every agent step, with sources" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code is here if you want to poke at it: &lt;a href="https://github.com/DEVJHAWAR11/GridMind" rel="noopener noreferrer"&gt;github.com/DEVJHAWAR11/GridMind&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>backend</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
