<?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: Ivan Stankovic</title>
    <description>The latest articles on DEV Community by Ivan Stankovic (@lognebudo).</description>
    <link>https://dev.to/lognebudo</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%2F3829335%2F909c5a1e-dcd9-4dbd-b691-b976cb0c1bbd.jpg</url>
      <title>DEV Community: Ivan Stankovic</title>
      <link>https://dev.to/lognebudo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lognebudo"/>
    <language>en</language>
    <item>
      <title>Ollama says my model does 13,826 tokens/sec. It does 43.</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Tue, 15 Sep 2026 22:14:04 +0000</pubDate>
      <link>https://dev.to/lognebudo/ollama-says-my-model-does-13826-tokenssec-it-does-43-54g</link>
      <guid>https://dev.to/lognebudo/ollama-says-my-model-does-13826-tokenssec-it-does-43-54g</guid>
      <description>&lt;p&gt;That number is not a typo, and my GPU has not improved. Both figures came out of the same daemon, from the same prompt, thirty seconds apart.&lt;/p&gt;

&lt;p&gt;If you have Ollama running, you can check this on your own machine before you read any further. Send the same prompt twice and print three fields:&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&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;num_predict&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/api/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&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;Content-Type&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;application/json&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&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;Context paragraph about nothing. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Say OK.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&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;cold&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;warm&lt;/span&gt;&lt;span class="sh"&gt;"&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;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_eval_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;prompt_eval_cached_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_eval_duration&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="mf"&gt;1e9&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;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; count=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cached=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;rate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tok/s&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;Here is what I get. Ollama 0.34.0, qwen2.5:7b, one machine, nothing else running:&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;&lt;code&gt;prompt_eval_count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_cached_count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_duration&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;count ÷ duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cold&lt;/td&gt;
&lt;td&gt;318&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;139.97 ms&lt;/td&gt;
&lt;td&gt;2,272 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;318&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;317&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23.00 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13,826 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row claims the model prefilled 318 tokens at nearly fourteen thousand tokens per second. It did not. It prefilled &lt;strong&gt;one&lt;/strong&gt; token and read the other 317 out of the KV cache. The honest prefill rate for that row is 43 tok/s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the number comes out wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prompt_eval_duration&lt;/code&gt; times only the tokens that were &lt;strong&gt;actually computed&lt;/strong&gt;.&lt;br&gt;
&lt;code&gt;prompt_eval_count&lt;/code&gt; still reports the &lt;strong&gt;total&lt;/strong&gt; prompt size, cached tokens included. So the obvious calculation divides a whole prompt by the time taken to process a fraction of it, and the error is not a small constant — it is exactly the cache hit ratio:&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;inflation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_eval_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_eval_count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prompt_eval_cached_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 317 of 318 tokens cached, that is 318x. Note what this means in practice: &lt;strong&gt;the better your cache is working, the more the number lies.&lt;/strong&gt; A cold prompt reports truthfully. A perfectly warm one is off by the length of your system prompt. Any dashboard plotting prefill tokens/sec over a conversation is drawing a curve of its own cache hit rate and labelling it throughput.&lt;/p&gt;

&lt;p&gt;The fix is one term:&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;uncached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;or&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;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uncached&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uncached&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="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also what Ollama's own &lt;code&gt;Metrics.Summary()&lt;/code&gt; does, so the daemon is not really disagreeing with itself — it just exposes two fields whose units stopped matching, and the older, more obvious one is the one everybody already had in their code.&lt;/p&gt;

&lt;p&gt;Two details worth having:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When &lt;code&gt;uncached&lt;/code&gt; is zero, report nothing.&lt;/strong&gt; A fully cached prompt has no prefill rate. Print an em dash, not &lt;code&gt;0&lt;/code&gt;, and not &lt;code&gt;∞&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absent is not zero.&lt;/strong&gt; Daemons before 0.33.3 omit &lt;code&gt;prompt_eval_cached_count&lt;/code&gt; entirely.
Defaulting a missing field to &lt;code&gt;0&lt;/code&gt; silently turns "I don't know" into a confident,   wrong claim that nothing was cached. Leave it undefined and say so in the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same fact under three different names
&lt;/h2&gt;

&lt;p&gt;Ollama exposes this through three API surfaces, and each calls it something else. All three verified on 0.34.0, same prompt, same warm cache:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;endpoint&lt;/th&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/api/chat&lt;/code&gt; (native)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prompt_eval_cached_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/v1/chat/completions&lt;/code&gt; (OpenAI-compatible)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usage.prompt_tokens_details.cached_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/v1/messages&lt;/code&gt; (Anthropic-compatible)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usage.cache_read_input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One trap on that last row. The Anthropic-compatible endpoint returned&lt;br&gt;
&lt;code&gt;input_tokens: 1&lt;/code&gt; for a 318-token prompt. &lt;code&gt;input_tokens&lt;/code&gt; there means &lt;em&gt;total minus cache reads&lt;/em&gt;, not the total — so reconstructing prompt size means adding the two together. If you are summing &lt;code&gt;input_tokens&lt;/code&gt; across turns to estimate load, warm turns will quietly contribute almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now that the cache is visible, it is worth measuring
&lt;/h2&gt;

&lt;p&gt;The useful consequence of a readable &lt;code&gt;cached_count&lt;/code&gt; is that you can finally see something most of us only assert: a local model reuses its KV cache only while the prompt matches &lt;strong&gt;from the very first token&lt;/strong&gt;. One volatile value near the top forfeits everything after it, every single turn.&lt;/p&gt;

&lt;p&gt;Same body text, 227 tokens, one timestamp. Turn one primes the cache; turn two sends the same layout with a &lt;em&gt;fresh&lt;/em&gt; timestamp, which is what real traffic does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;timestamp position&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;reused&lt;/th&gt;
&lt;th&gt;prefill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;front&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;41.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;back&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;211&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.3 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Identical words, identical model, 2.5x the prefill time, purely from where the changing value sits. The 40 tokens still reused in the front case are the chat template's own stable preamble, not your content — everything you wrote is recomputed.&lt;/p&gt;

&lt;p&gt;So the rule is boring and worth following anyway: &lt;strong&gt;stable text first, volatile text last.&lt;/strong&gt; Current date, session id, user name, retrieved chunks that change per turn — push them to the end of the prompt, below anything that stays put.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement I got wrong first
&lt;/h2&gt;

&lt;p&gt;I want to flag this, because I spent a day producing a confidently inverted result.&lt;/p&gt;

&lt;p&gt;My first attempt asked: &lt;em&gt;how much of layout A's cache does layout B reuse?&lt;/em&gt; I primed with one layout, sent the other, and read the hit count. It said the original prompt was getting 323 of 324 tokens cached and my improved rewrite was getting 3 — that the fix made things four times worse.&lt;/p&gt;

&lt;p&gt;The question was wrong. Nobody alternates between two prompt layouts. Real traffic sends &lt;strong&gt;the same layout every turn with a fresh value in it&lt;/strong&gt;, so each layout has to be measured against &lt;em&gt;itself&lt;/em&gt;: send it twice, mutate the volatile value between the sends, and take the &lt;strong&gt;second&lt;/strong&gt; send as the measurement. Measuring the first send only tells you how well a prompt matches itself, which is ~100% for every layout ever written and tells you nothing.&lt;/p&gt;

&lt;p&gt;The thing I keep taking from this: the bug was not in the arithmetic, and no unit test would have caught it, because the code did exactly what I asked. Only the live daemon could tell me I was asking the wrong question. If you are measuring cache behaviour, prime it and mutate it — a single cold send measures nothing you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go and check your own numbers
&lt;/h2&gt;

&lt;p&gt;The two scripts above are the whole point of this post; everything else is commentary on what they print. If you are running an observability dashboard, a benchmark harness, or anything that reports prefill tokens/sec for a local model, it is worth thirty seconds to find out whether it divides by &lt;code&gt;prompt_eval_count&lt;/code&gt; or by &lt;code&gt;prompt_eval_count - prompt_eval_cached_count&lt;/code&gt;. Mine divided by the wrong one for months.&lt;/p&gt;

&lt;p&gt;Numbers here are from a single machine (Ollama 0.34.0, qwen2.5:7b, 2026-09-16) and yours will differ — the inflation factor in particular is just your cache hit ratio, so it scales with how long your system prompt is. The direction of the effect is not machine-specific.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/LogneBudo/llmxray" rel="noopener noreferrer"&gt;LLMxRay&lt;/a&gt;, a local LLM observatory that now surfaces the cached counts across all three protocols and has a page for running the prime-and-mutate test against your own daemon. It is Apache-2.0, local-only, and there is nothing to sign up for. But the fields are in the API whether or not you use anything of mine, and&lt;br&gt;
the one-term fix above is the part that actually matters.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Stop Guessing, Start Seeing: Multi-Model Observability with LLMxRay 🕵️‍♂️</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Fri, 03 Apr 2026 20:55:02 +0000</pubDate>
      <link>https://dev.to/lognebudo/stop-guessing-start-seeing-multi-model-observability-with-llmxray-1djh</link>
      <guid>https://dev.to/lognebudo/stop-guessing-start-seeing-multi-model-observability-with-llmxray-1djh</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fmu8knu7mylfhykwoe7ty.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.amazonaws.com%2Fuploads%2Farticles%2Fmu8knu7mylfhykwoe7ty.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you ever wondered why the same prompt costs more in one language than another? Or why a model feels "smarter" in English but struggles with Arabic or Chinese?&lt;/p&gt;

&lt;p&gt;When working with LLMs, we often treat the response as a black box. We see the output, but we don't see the mechanics—the tokenization, the side-by-side comparison of different model families, or how different writing systems affect performance.&lt;/p&gt;

&lt;p&gt;I built LLMxRay to pull back the curtain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is LLMxRay?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLMxRay is an open-source observability tool designed to help developers inspect how different LLMs handle the exact same prompt in real-time. Whether you are using local models via Ollama/LM Studio or cloud-based APIs, LLMxRay gives you a "side-by-side" X-ray view of your prompt's journey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multi-Model Comparison: Run one prompt against multiple models simultaneously. See how Llama 3 compares to Mistral or GPT-4o in one view.&lt;/p&gt;

&lt;p&gt;Multilingual Deep-Dive: This was a big focus for me. The tool supports 4 languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English 🇺🇸&lt;/li&gt;
&lt;li&gt;French 🇫🇷&lt;/li&gt;
&lt;li&gt;Arabic 🇸🇦 (RTL support)&lt;/li&gt;
&lt;li&gt;Chinese 🇨🇳&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tokenization Transparency: See exactly how your text is being chopped up into tokens. This is crucial for debugging cost, context window limits, and model "reasoning" quality across different writing systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 4 Languages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tokenization isn't equal. A single concept might be 1 token in English but 3 tokens in another language. By supporting Latin, RTL (Arabic), and character-based (Chinese) scripts, LLMxRay lets you see the economic and technical difference of running multilingual apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it out&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is early-stage and open for feedback! You can connect it to your local environment or use your API keys to start comparing models immediately.&lt;/p&gt;

&lt;p&gt;👉 Check out the repo here: &lt;br&gt;
&lt;a href="https://github.com/LogneBudo/llmxray" rel="noopener noreferrer"&gt;https://github.com/LogneBudo/llmxray&lt;/a&gt;&lt;br&gt;
or website and docs here:&lt;br&gt;
&lt;a href="https://lognebudo.github.io/llmxray/" rel="noopener noreferrer"&gt;https://lognebudo.github.io/llmxray/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear from the DEV community:&lt;/p&gt;

&lt;p&gt;Which model families do you want to see compared next?&lt;/p&gt;

&lt;p&gt;Are there specific visualizations that would help your LLM workflow?&lt;/p&gt;

&lt;p&gt;Drop a comment below or open an issue on GitHub! 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>LLMxRay: A Local Observatory for Understanding How LLMs Think</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Tue, 17 Mar 2026 11:47:10 +0000</pubDate>
      <link>https://dev.to/lognebudo/llmxray-a-local-observatory-for-understanding-how-llms-think-4pj8</link>
      <guid>https://dev.to/lognebudo/llmxray-a-local-observatory-for-understanding-how-llms-think-4pj8</guid>
      <description>&lt;p&gt;Modern LLMs generate impressive results, but the most interesting part isn’t the final answer — it’s everything that happens before the answer appears.&lt;br&gt;
Token probabilities, confidence shifts, reasoning traces, tool calls, divergences between models… all of this is usually hidden.&lt;br&gt;
I wanted a way to see these internals clearly, locally, and without relying on cloud APIs.&lt;br&gt;
That’s how LLMxRay started.&lt;/p&gt;

&lt;p&gt;🧠 What LLMxRay does&lt;br&gt;
LLMxRay is a local-first observability tool for LLMs.&lt;br&gt;
It works with Ollama, LM Studio, llama.cpp, and any endpoint that streams tokens.&lt;br&gt;
It gives you a real-time view of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token-by-token generation with confidence heatmaps&lt;/li&gt;
&lt;li&gt;Reasoning traces (when the model exposes them)&lt;/li&gt;
&lt;li&gt;Side-by-side model comparison&lt;/li&gt;
&lt;li&gt;Tool/function call execution&lt;/li&gt;
&lt;li&gt;Latency and cost breakdowns&lt;/li&gt;
&lt;li&gt;Agent behavior introspection&lt;/li&gt;
&lt;li&gt;A built-in Tools Workshop to design and test function-calling &lt;/li&gt;
&lt;li&gt;flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs locally.&lt;br&gt;
No cloud, no telemetry, no accounts.&lt;/p&gt;

&lt;p&gt;🔍 Why I built it&lt;/p&gt;

&lt;p&gt;Working with local models, I often needed to answer questions like:&lt;br&gt;
• Why did this model choose this token?&lt;br&gt;
• Where did the reasoning diverge?&lt;br&gt;
• Why does Q4_K_M behave differently from Q6_K?&lt;br&gt;
• What exactly happened during a tool call?&lt;br&gt;
• How do two models respond to the same prompt internally?&lt;/p&gt;

&lt;p&gt;Existing UIs focus on chat experience, not introspection.&lt;br&gt;
Debugging required custom scripts, logs, or guesswork.&lt;br&gt;
LLMxRay tries to make this transparent.&lt;/p&gt;

&lt;p&gt;🛠️ How it works&lt;/p&gt;

&lt;p&gt;LLMxRay sits between you and your local model:&lt;/p&gt;

&lt;p&gt;• It captures the token stream&lt;br&gt;
• It records probabilities and reasoning (if available)&lt;br&gt;
• It visualizes everything in a clean, interactive UI&lt;br&gt;
• It stores traces so you can compare runs&lt;br&gt;
• It supports multiple models and endpoints&lt;br&gt;
You can run it with:&lt;/p&gt;

&lt;p&gt;Or clone the repo and run it locally.&lt;/p&gt;

&lt;p&gt;📦 Links&lt;/p&gt;

&lt;p&gt;• GitHub: &lt;a href="https://github.com/lognebudo/llmxray" rel="noopener noreferrer"&gt;https://github.com/lognebudo/llmxray&lt;/a&gt;&lt;br&gt;
• Demo / docs: &lt;a href="https://lognebudo.github.io/llmxray/" rel="noopener noreferrer"&gt;https://lognebudo.github.io/llmxray/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💬 What’s next&lt;br&gt;
I’m working on:&lt;/p&gt;

&lt;p&gt;• better comparison tools&lt;br&gt;
• an education pillar with kits for teachers and students&lt;br&gt;
• improved reasoning visualization&lt;br&gt;
• support for more local runtimes&lt;/p&gt;

&lt;p&gt;If you work with local models, I’d love to hear how you debug or introspect them — and what features would help you the most.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>learning</category>
      <category>news</category>
    </item>
  </channel>
</rss>
