<?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: ethanlin</title>
    <description>The latest articles on DEV Community by ethanlin (@ethanjlin).</description>
    <link>https://dev.to/ethanjlin</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%2F4046015%2F44eea633-e6e6-4e33-b45a-839c0eef8e49.png</url>
      <title>DEV Community: ethanlin</title>
      <link>https://dev.to/ethanjlin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethanjlin"/>
    <language>en</language>
    <item>
      <title>Your 256K Context Window Is a Ceiling, Not a Budget</title>
      <dc:creator>ethanlin</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:07:59 +0000</pubDate>
      <link>https://dev.to/ethanjlin/your-256k-context-window-is-a-ceiling-not-a-budget-5cc2</link>
      <guid>https://dev.to/ethanjlin/your-256k-context-window-is-a-ceiling-not-a-budget-5cc2</guid>
      <description>&lt;p&gt;Gemma 4 ships with a context window up to 256K tokens. Every time a model lands with a headline context number, the same thing happens: people paste their entire corpus into the prompt, watch latency and memory explode, get mediocre answers, and conclude long context "doesn't work."&lt;/p&gt;

&lt;p&gt;The window is a ceiling, not a target. Here is the mechanical reason why, and what I do instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the cost lives: attention vs. KV cache
&lt;/h2&gt;

&lt;p&gt;Two different costs scale with sequence length, and people conflate them constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attention compute&lt;/strong&gt; is the O(n²) one everybody quotes. For a sequence of length n, full self-attention compares every token to every other token. Double the input, quadruple the attention work. This dominates &lt;em&gt;prefill&lt;/em&gt; — the pass where the model ingests your prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KV cache memory&lt;/strong&gt; is O(n) and it is the one that actually kills you in production. Every token you process leaves behind key and value tensors that must stay resident for the rest of generation. Rough shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kv_bytes ≈ 2 (K and V)
         × n_layers
         × n_kv_heads × head_dim
         × seq_len
         × bytes_per_element
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what is &lt;em&gt;not&lt;/em&gt; in there: batch size, which multiplies the whole thing. Eight concurrent 200K-token requests means eight of those allocations live simultaneously. This is why a model that runs fine at 8K falls over at 128K with three users — you did not run out of weights memory, you ran out of cache.&lt;/p&gt;

&lt;p&gt;Gemma 4's architecture is a direct response to this. Both workstation-class models interleave local sliding-window attention with periodic full-global attention layers, with the final layer always global. The consequence is worth internalizing: &lt;strong&gt;only the global layers pay full O(n²) and hold a full-length KV cache.&lt;/strong&gt; The sliding-window layers attend within a bounded window, so their cache is capped at window size regardless of how long your input is.&lt;/p&gt;

&lt;p&gt;So the cost curve is not the naive one. If one layer in every k is global, your per-layer cache length is:&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_len_per_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;global_every&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;global_every&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;seq_len&lt;/span&gt;              &lt;span class="c1"&gt;# unbounded, grows with input
&lt;/span&gt;    &lt;span class="k"&gt;return&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;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# capped, flat after the window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is genuinely good engineering and it is why 256K is feasible at all. It is not a license to use 256K.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why long context degrades before it OOMs
&lt;/h2&gt;

&lt;p&gt;Even with the memory solved, quality does not stay flat as you fill the window. Two well-documented effects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Positional degradation.&lt;/strong&gt; Retrieval accuracy is not uniform across the window. Information at the very start and very end of a long prompt is recovered reliably; material buried in the middle is recovered noticeably worse. This is the "lost in the middle" behavior observed across essentially every long-context model family — a property of how attention distributes mass, not a bug in any one model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distractor dilution.&lt;/strong&gt; Adding irrelevant-but-plausible text alongside the correct passage measurably degrades answers. If your true evidence is 2K tokens and you pad it to 180K with semantically adjacent noise, you gave the model more places to be wrong.&lt;/p&gt;

&lt;p&gt;Then there is latency. Prefill on a very long prompt is a large one-time compute cost that lands entirely before the first token appears, and users experience it as the app hanging. Decode speed after prefill is barely affected — a genuinely confusing profile if you have not seen it. It feels fast once it starts. It just takes forever to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retrieve first, then use the big window as slack.&lt;/strong&gt; The point of 256K is not to skip retrieval — it is that your retriever no longer has to be surgical. Under a 4K budget you needed the top-3 chunks and precision mattered enormously. Now you can pull top-40, include full surrounding sections instead of severed 512-token fragments, and stop building elaborate re-rankers. &lt;strong&gt;Long context buys you slack in retrieval precision, not permission to skip retrieval.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunk on structure, not character count.&lt;/strong&gt; Fixed-size chunking splits mid-argument and mid-table. Split on headings, sections, and function boundaries, and carry the heading path into each chunk so a retrieved fragment knows where it came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put instructions last.&lt;/strong&gt; Given positional degradation, the layout that has consistently worked for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;system&lt;/span&gt; / &lt;span class="n"&gt;role&lt;/span&gt;]
[&lt;span class="n"&gt;long&lt;/span&gt; &lt;span class="n"&gt;retrieved&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;]      &amp;lt;- &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;bulk&lt;/span&gt;, &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;middle&lt;/span&gt;
[&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;]
[&lt;span class="n"&gt;explicit&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;]  &amp;lt;- &lt;span class="n"&gt;last&lt;/span&gt;, &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="n"&gt;generation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instructions at the top of a 100K-token prompt are competing with 100K tokens of recency. Put them where the model is looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache the stable prefix.&lt;/strong&gt; If many requests share a large fixed preamble — a schema, a policy doc, a codebase index — order the prompt so that block is a byte-identical prefix and reuse its KV cache. This is often a bigger latency win than any model swap. It also means: never put a timestamp or request ID at the top of your prompt. One varying token at position 0 invalidates the entire prefix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget explicitly and measure at your real length.&lt;/strong&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="n"&gt;BUDGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32_000&lt;/span&gt;   &lt;span class="c1"&gt;# not 256_000
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUDGET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;reserve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2_000&lt;/span&gt;            &lt;span class="c1"&gt;# instructions + room to generate
&lt;/span&gt;    &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kept&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="p"&gt;[]&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="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="c1"&gt;# ranked, best first
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;used&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="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;kept&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;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;used&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="n"&gt;tokens&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then evaluate at the length you actually serve. A benchmark at 4K tells you nothing about behavior at 120K, and needle-in-a-haystack scores tell you nothing about multi-hop reasoning across scattered evidence. For the per-size architecture specs you need to do this math on your own hardware, the &lt;a href="https://gemma-4.net" rel="noopener noreferrer"&gt;Gemma 4 model hub&lt;/a&gt; collects the family breakdown in one place.&lt;/p&gt;

&lt;p&gt;My working default: start at 8–32K, raise the ceiling only when evals demonstrate the extra context earns its latency. Treat any prompt that fills more than half the window as a design smell — it usually means retrieval is doing nothing and you are paying for the model to do the filtering you should have done.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>performance</category>
      <category>python</category>
    </item>
    <item>
      <title>Picking a Gemma 4 Quantization: VRAM Math That Actually Matters</title>
      <dc:creator>ethanlin</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:00:12 +0000</pubDate>
      <link>https://dev.to/ethanjlin/picking-a-gemma-4-quantization-vram-math-that-actually-matters-1f0b</link>
      <guid>https://dev.to/ethanjlin/picking-a-gemma-4-quantization-vram-math-that-actually-matters-1f0b</guid>
      <description>&lt;p&gt;Every "run this model locally" guide tells you to grab a Q4 GGUF and move on. That advice is fine right up until you try a long-context run and your machine starts swapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weights are the part everyone budgets for
&lt;/h2&gt;

&lt;p&gt;Quantization maths is straightforward. A model's weight footprint is roughly &lt;code&gt;params x bits / 8&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quant&lt;/th&gt;
&lt;th&gt;Bits/param&lt;/th&gt;
&lt;th&gt;12B model&lt;/th&gt;
&lt;th&gt;Quality note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;~8.5&lt;/td&gt;
&lt;td&gt;~12.8 GB&lt;/td&gt;
&lt;td&gt;Near-lossless, rarely worth it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;~6.6&lt;/td&gt;
&lt;td&gt;~9.9 GB&lt;/td&gt;
&lt;td&gt;Very close to Q8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;~4.8&lt;/td&gt;
&lt;td&gt;~7.2 GB&lt;/td&gt;
&lt;td&gt;The usual sweet spot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;~3.9&lt;/td&gt;
&lt;td&gt;~5.9 GB&lt;/td&gt;
&lt;td&gt;Noticeable degradation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Below Q4 the loss stops being subtle. Instruction-following degrades before raw perplexity does, which is why benchmark numbers can look fine while the model quietly stops respecting your system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The KV cache is the part that bites
&lt;/h2&gt;

&lt;p&gt;Here is what the guides skip. The KV cache scales with &lt;strong&gt;context length&lt;/strong&gt;, and it is not quantized by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kv_bytes ~= 2 (K and V) x layers x kv_heads x head_dim x seq_len x dtype_bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical consequence: a model that loads in 7 GB can need well over twice that at long context. Grouped-query attention helps a lot — &lt;code&gt;kv_heads&lt;/code&gt; is much smaller than attention heads — but the term still grows linearly with sequence length while your weights stay fixed.&lt;/p&gt;

&lt;p&gt;Two knobs matter more than picking a fancier quant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--ctx-size&lt;/code&gt;&lt;/strong&gt;: do not allocate 128K if your prompts are 8K. You are reserving memory you will never touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV cache quantization&lt;/strong&gt; (&lt;code&gt;q8_0&lt;/code&gt; for K/V): roughly halves cache memory for a quality hit most workloads never notice. Underused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A decision order that works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start at Q4_K_M&lt;/li&gt;
&lt;li&gt;Set context to what you actually use, not the model maximum&lt;/li&gt;
&lt;li&gt;If you are still tight, quantize the KV cache before dropping to Q3&lt;/li&gt;
&lt;li&gt;Only move up to Q6/Q8 if you have headroom left over&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That ordering matters: dropping to Q3 to buy context is the most common mistake, and it trades a permanent quality loss for memory you could have gotten from the cache instead.&lt;/p&gt;

&lt;p&gt;Per-quantization benchmarks and deployment notes for the Gemma 4 family are collected at &lt;a href="https://gemma-4.net" rel="noopener noreferrer"&gt;gemma-4.net&lt;/a&gt; — every number self-run, raw data published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;These are rules of thumb, not guarantees. Backends differ in how they allocate, and Apple Silicon unified memory behaves differently from discrete VRAM. Measure on your own hardware before trusting any table, including mine.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>opensource</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
  </channel>
</rss>
