<?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: Rickesh T N</title>
    <description>The latest articles on DEV Community by Rickesh T N (@rickeshtn).</description>
    <link>https://dev.to/rickeshtn</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%2F4066262%2Fafe79880-1a39-479b-b6f1-0a5d39b9802a.jpeg</url>
      <title>DEV Community: Rickesh T N</title>
      <link>https://dev.to/rickeshtn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rickeshtn"/>
    <language>en</language>
    <item>
      <title>My inference server decided my second GPU no longer exists. Here is how I got it back without upgrading a driver.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:29:36 +0000</pubDate>
      <link>https://dev.to/rickeshtn/my-inference-server-decided-my-second-gpu-no-longer-exists-here-is-how-i-got-it-back-without-hhp</link>
      <guid>https://dev.to/rickeshtn/my-inference-server-decided-my-second-gpu-no-longer-exists-here-is-how-i-got-it-back-without-hhp</guid>
      <description>&lt;p&gt;My desktop runs two mismatched GPUs: a 20GB Ampere card and a 16GB Pascal Tesla. For months, a 21GB vision-language model ran split across both at full GPU speed. Then a routine upgrade of the inference server dropped Pascal support behind a driver-version gate, demoted the old card to a Vulkan device the scheduler refuses to mix with CUDA, and silently rescheduled my model to one GPU plus CPU spill.&lt;/p&gt;

&lt;p&gt;Nothing errored. The model still answered. It just answered at 20 to 40 seconds per item instead of 8, which turned a 12-hour benchmark into a 60-hour one. The only evidence was a log line saying the driver was "too old" for a card that had been running CUDA workloads that same morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate is policy, not physics
&lt;/h2&gt;

&lt;p&gt;The interesting discovery came from reading the server's own startup logs: the bundled CUDA kernels still listed the Pascal compute capability in their build targets. The kernels existed. The Go-side scheduler was refusing to use them based on a driver-version check, while the C++ inference runtime underneath had no such opinion.&lt;/p&gt;

&lt;p&gt;That asymmetry is the exploit. The upgraded server ships its actual inference engine as a standalone binary, a vendored build of llama-server, with its CUDA backend as a dynamically loaded library sitting in a subdirectory. The driver gate lives entirely in the scheduler process that launches it.&lt;/p&gt;

&lt;p&gt;So: launch the vendored binary directly.&lt;/p&gt;

&lt;p&gt;Two mechanics matter. First, the dynamic backend is not found automatically because it lives in a subdirectory the loader does not scan; it needs an explicit environment variable pointing at the .so file itself, not the directory. Get that wrong and the binary silently falls back to CPU while still accepting requests, which cost me one confusing benchmark restart. Second, the vendored build understands the server's own single-file model format, weights and vision projector packed together, which upstream llama.cpp of the same vintage refuses to load. The vendored binary logged "detected combined format, translating" and just worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tensor splits are a policy decision too
&lt;/h2&gt;

&lt;p&gt;With both GPUs visible again, the default split put layers proportional to total VRAM. That is wrong for mismatched cards: the Ampere card is roughly four times faster per layer, so every layer on the Pascal card costs four on the fast one. Skewing the split heavily toward the fast card, leaving the slow card holding only what does not fit, took per-item latency from 17 seconds to 9. Combined with a bigger prefill batch, the full benchmark ran in 12 hours instead of a projected 60-plus.&lt;/p&gt;

&lt;p&gt;The general shape of the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scheduler said no; the runtime said yes. When a managed serving layer refuses your hardware, check whether the engine underneath actually shares the objection.&lt;/li&gt;
&lt;li&gt;Point the backend loader at the exact library file and verify placement with nvidia-smi before trusting any run. A server that fell back to CPU serves identical responses, slower.&lt;/li&gt;
&lt;li&gt;On mismatched GPUs, split by throughput, not by capacity. Free VRAM on a slow card is not free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clean fix is a driver upgrade, and it is scheduled. But the bypass took forty minutes including the two failed attempts, needed no root, touched nothing system-wide, and taught me more about the serving stack than a year of it working silently ever did. The scheduler is there to protect the average user from edge cases. If you can read its logs, you are allowed to disagree with it.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>llamacpp</category>
      <category>ollama</category>
      <category>homelab</category>
    </item>
    <item>
      <title>A generic fine-tuning playbook, written after doing it wrong several times</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:29:00 +0000</pubDate>
      <link>https://dev.to/rickeshtn/a-generic-fine-tuning-playbook-written-after-doing-it-wrong-several-times-37a3</link>
      <guid>https://dev.to/rickeshtn/a-generic-fine-tuning-playbook-written-after-doing-it-wrong-several-times-37a3</guid>
      <description>&lt;p&gt;Every fine-tuning guide I read before my first serious attempt was a tutorial about knobs: learning rates, LoRA ranks, quantization settings. None of them covered the part that actually decides whether the project succeeds, which happens before and after the training run, not during it. This is the playbook I now follow for any model on any task. It is deliberately generic: the same sequence has carried me through vision-language models on driving data, small text models for domain QA, and RL-style preference tuning, on hardware ranging from a single consumer GPU to rented cloud boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: try not to fine-tune
&lt;/h2&gt;

&lt;p&gt;Fine-tuning is the most expensive intervention in the stack, so it goes last. The ladder, cheapest first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Better prompting.&lt;/strong&gt; A system prompt with three good few-shot examples routinely closes half the gap that people reach for fine-tuning to close. It costs an afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval.&lt;/strong&gt; If the failure is missing knowledge rather than missing behavior, RAG beats weights. Knowledge changes; your fine-tune will not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bigger or different base model.&lt;/strong&gt; Run the comparison honestly: a zero-shot larger model against your imagined fine-tuned smaller one. I have watched an untouched open-weights model beat an in-domain fine-tune on the fine-tune's own benchmark. It happens more often than leaderboards suggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tune.&lt;/strong&gt; Only when the behavior you need is demonstrably not in the base model and cannot be retrieved or prompted in: output format compliance, domain-specific reasoning patterns, a persona that must survive thousands of turns, latency budgets that force a small model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Write down, in one sentence, which failure of steps 1 to 3 justifies the fine-tune. If you cannot write that sentence, stop. That sentence also becomes your evaluation target later, which is the real reason to write it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: build the evaluation before the dataset
&lt;/h2&gt;

&lt;p&gt;This ordering feels backwards and is the single highest-leverage decision in the playbook.&lt;/p&gt;

&lt;p&gt;Before collecting training data, build a held-out evaluation that measures the sentence from step 0, and run the &lt;strong&gt;base model&lt;/strong&gt; through it. That number is your baseline, and it does three jobs. It tells you the true size of the gap. It occasionally kills the project on the spot because the base model was already good enough and nobody had measured it. And it validates the harness itself: an evaluation that has never scored a known model is untested code that emits numbers.&lt;/p&gt;

&lt;p&gt;Two rules for the eval that I no longer break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The test split is designed before training, and nothing from training may touch it.&lt;/strong&gt; Not for hyperparameter selection, not for checkpoint picking, not once. Use a validation split for those. If your splits share source documents, scenes, or sessions with the training set, you are measuring memorization and calling it generalization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run a sanity control.&lt;/strong&gt; Delete the input and measure again. If a vision model scores far above chance with the images removed, your benchmark leaks answers through priors and question phrasing, and every score it has produced is inflated. The same control exists for text: shuffle the context, drop the retrieved passages, feed the question alone. Cheap to run, devastating when it fires, and better fired at you than at a reviewer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: data is the model
&lt;/h2&gt;

&lt;p&gt;The dataset decides what you get. The recipe decides only how efficiently you get it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A few thousand excellent examples beat a hundred thousand scraped ones&lt;/strong&gt; for behavior tuning. For format compliance and persona, hundreds can be enough with LoRA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate against your evaluation.&lt;/strong&gt; Near-duplicates leak. Exact-match dedup is not sufficient; hash at the level the data actually repeats (documents, scenes, sessions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit a random hundred by hand.&lt;/strong&gt; Not the first hundred, a random hundred. The first hundred were curated by whoever built the file; the random hundred tell you the truth about label noise. Every bad label teaches the model confidently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the training distribution to the inference distribution.&lt;/strong&gt; If production inputs will be messy, OCR-damaged, or truncated, train on that, not on the clean version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expect data work to consume more than half the project's wall-clock. When it does not, that is usually a sign it was skipped, and the bill arrives later, denominated in GPU-hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: choose the lightest recipe that can express the change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LoRA or QLoRA&lt;/strong&gt; is the default. Behavior shaping, format compliance, domain adaptation, personas: adapters handle all of it at a fraction of the memory, and the artifact is small enough to version, ship, stack, and roll back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full fine-tuning&lt;/strong&gt; is for when the change is deep: new modalities, new tokenizers, or when adapters measurably plateau below target. Prove the plateau before paying for the parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preference or RL methods&lt;/strong&gt; (DPO, GRPO and relatives) are for objectives that supervised examples cannot express: relative quality, verifiable rewards, multi-step outcomes. They are also where silent failure lives, so they come with an extra rule: track a metric that measures the actual objective, not a proxy. I once watched token accuracy sit at 99 percent while the policy learned the opposite of the intended behavior. The proxy was fine; the behavior was not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One decision that outranks the recipe choice: &lt;strong&gt;change one variable at a time.&lt;/strong&gt; A run that changes base model, dataset, rank, and learning rate simultaneously produces a result that cannot be attributed to anything. Reviewers catch this in papers; production catches it in incidents. If you must move fast, move fast serially.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: engineer the run like it will be interrupted, because it will be
&lt;/h2&gt;

&lt;p&gt;Training runs die. Disks unmount, drivers hiccup, a colleague's job lands on your GPU, spot instances vanish. The runs that survive share the same boring infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint on a fixed cadence and verify resume actually works&lt;/strong&gt; before the long run, not during the outage. A checkpoint you have never resumed from is a hope, not a checkpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log to an experiment tracker, not a terminal.&lt;/strong&gt; Scalars, config, git commit, environment. The question you will ask in three weeks is "what exactly produced this file", and scrollback does not answer it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emit progress with the failure count first.&lt;/strong&gt; A silent job that is working and a silent job that is failing on every batch look identical from outside. If the error count is climbing, you want to see it in the first minute, not after the run completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the environment.&lt;/strong&gt; Container images beat requirements files; requirements files beat memory. Half of my hardest debugging sessions were two libraries disagreeing about something as small as position ids, and the fix was environmental, not algorithmic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: evaluate like an adversary, then decide
&lt;/h2&gt;

&lt;p&gt;When training finishes, resist the demo. Run the same held-out evaluation from step 1, and read three numbers together, never one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Target metric versus base model.&lt;/strong&gt; Did the gap from step 0 actually close, with the improvement larger than your seed-to-seed noise? If you have not measured seed noise, run the fine-tune twice before believing any margin smaller than a point or two. Single-seed margins evaporate embarrassingly often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression suite.&lt;/strong&gt; What did the model lose? General capability regressions are the default outcome of narrow fine-tuning, not the exception. A small fixed battery of out-of-domain checks is enough to catch the worst of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train-versus-held-out gap.&lt;/strong&gt; Large gap: you memorized; get more data or regularize. No gap and no improvement: capacity or recipe; move up the ladder from step 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then make the deployment decision with the same honesty as step 0: is the fine-tuned model better than the best non-fine-tuned alternative, at the quality, latency, and cost that production actually needs? Sometimes the answer is no, and the fine-tune becomes a lesson rather than a deployment. That outcome is not a failure of the playbook. Discovering it for the price of one training run instead of one production incident is the playbook working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-page version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Prompt, retrieve, or upgrade the base model first; fine-tune last, and write the sentence that justifies it.&lt;/li&gt;
&lt;li&gt;Build the eval and score the base model before touching training data. Design the splits before training. Run the deleted-input control.&lt;/li&gt;
&lt;li&gt;Spend most of the project on data: dedup against eval, audit random samples, match production distribution.&lt;/li&gt;
&lt;li&gt;Default to adapters; escalate only on proven plateaus; one variable per run.&lt;/li&gt;
&lt;li&gt;Checkpoint, track, and emit errors-first progress; pin the environment.&lt;/li&gt;
&lt;li&gt;Judge with three numbers: target delta versus noise, regressions, generalization gap. Then decide like you have not already sunk the cost, because the model does not care that you did.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>llm</category>
      <category>finetuning</category>
      <category>machinelearning</category>
      <category>mlops</category>
    </item>
    <item>
      <title>A 4B model on a 6GB laptop beat Claude Opus on our 440K-token corpus. The fix was giving the model less to do.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:28:23 +0000</pubDate>
      <link>https://dev.to/rickeshtn/a-4b-model-on-a-6gb-laptop-beat-claude-opus-on-our-440k-token-corpus-the-fix-was-giving-the-model-52eo</link>
      <guid>https://dev.to/rickeshtn/a-4b-model-on-a-6gb-laptop-beat-claude-opus-on-our-440k-token-corpus-the-fix-was-giving-the-model-52eo</guid>
      <description>&lt;p&gt;Private AI has a hardware story nobody measures honestly. The pitch is that your data never leaves the building. The unstated cost is that the building contains a 6GB laptop GPU, and the corpus is 440,000 tokens that a frontier model would swallow in one call.&lt;/p&gt;

&lt;p&gt;We measured the whole thing. One RTX 3060 Laptop, 6144 MiB. One 2,625-record driving-QA dataset rendered to 957,493 characters. One question with a single correct answer: which category is least common. Ground truth computed in Python, so neither model gets to define success.&lt;/p&gt;

&lt;p&gt;The 4B won. It took four failed attempts to understand why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The context problem was never the hard part
&lt;/h2&gt;

&lt;p&gt;Direct context on that card, measured with flash attention and a q8_0 KV cache:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;num_ctx&lt;/th&gt;
&lt;th&gt;resident&lt;/th&gt;
&lt;th&gt;fits 5.5GB usable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;3.2 GB&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,384&lt;/td&gt;
&lt;td&gt;3.3 GB&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;3.3 GB&lt;/td&gt;
&lt;td&gt;yes, the ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;10.4 GB&lt;/td&gt;
&lt;td&gt;no, 31 percent on GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;131,072&lt;/td&gt;
&lt;td&gt;10.9 GB&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;KV is nearly free to 32k, then the allocator falls off a cliff. Switching the KV cache to q4_0 changed those numbers not at all, so the cliff is not the KV cache and you cannot quantize your way past it. &lt;strong&gt;Direct ceiling: 32,768 tokens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now stream a 261,226-token corpus through the same card in chunks, keeping the text in CPU RAM and only ever showing the model a fragment. Peak GPU across four runs: &lt;strong&gt;4.23, 4.24, 4.25, 4.54 GB.&lt;/strong&gt; Every one fits.&lt;/p&gt;

&lt;p&gt;That is an 8x context multiple at constant VRAM. Constant is the load-bearing word: the corpus never enters the KV cache, so the limit stops being memory and becomes wall-clock.&lt;/p&gt;

&lt;p&gt;Which is a solved problem. The unsolved one was that the answers were wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to fail the same question
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;attempt&lt;/th&gt;
&lt;th&gt;sub-calls&lt;/th&gt;
&lt;th&gt;answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;prose describing the data, having read about a third&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Spatial&lt;/code&gt; — right arithmetic, truncated label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Counterfactual&lt;/code&gt; — swept everything, well-formed, wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Status: beta, Status: delta, Status: gamma, Status: alpha&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Attempt 4 is the one that gives it away. Asked which label was least common, the model &lt;strong&gt;listed all four candidates instead of selecting one&lt;/strong&gt;. Attempt 3 had read all 425,054 input tokens and still combined the partial counts wrongly.&lt;/p&gt;

&lt;p&gt;We had been calling this a capability gap. It was not. A 4B can count rows in a fragment. What it cannot reliably do is plan a traversal and then perform arithmetic across 65 partial results — and we had been asking it to do both, inside a loop, while also formatting an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: treat the corpus like a video stream
&lt;/h2&gt;

&lt;p&gt;Nothing about aggregation requires a language model. So we stopped asking one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;video streaming&lt;/th&gt;
&lt;th&gt;the port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;manifest / playlist&lt;/td&gt;
&lt;td&gt;segment plan, computed in code before any model call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buffer&lt;/td&gt;
&lt;td&gt;N segments in flight, latency hidden behind compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decoder&lt;/td&gt;
&lt;td&gt;model sees ONE segment, emits &lt;code&gt;key&amp;lt;TAB&amp;gt;number&lt;/code&gt;, never prose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;playback&lt;/td&gt;
&lt;td&gt;reduce phase — aggregation in code, strategy chosen explicitly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model's entire job becomes extraction from a window it comfortably fits. Planning is deterministic. Summation is a loop. Selecting the minimum is one comparison.&lt;/p&gt;

&lt;p&gt;Result on the same corpus, same 4B, same 6GB card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 segments · failed=0 · records=611 · unparsed_lines=3 · keys=15 · 641s
Category: Spatial Relationship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct. 15 keys recovered, matching the 15 real categories. Of 611+ emitted lines, 3 failed the output contract and were counted as failures rather than silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontier comparison, which is not flattering to the frontier
&lt;/h2&gt;

&lt;p&gt;We used Claude Opus over the full 439,742-token context as the reference. It answered the same question two different ways on &lt;strong&gt;byte-identical input&lt;/strong&gt; — correct once, wrong once — and landed 2 out of 3 across three samples, at $4.79 per call.&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;answer&lt;/th&gt;
&lt;th&gt;correct&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 4.8, one call, 439,742 tok&lt;/td&gt;
&lt;td&gt;varies by run&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;$4.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4B + streaming, 6GB laptop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Spatial Relationship&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A reference that disagrees with itself is not an oracle. Ground truth has to come from code, with the frontier model scored as just another candidate.&lt;/p&gt;

&lt;p&gt;For private AI this cuts two ways. The bar is lower than the marketing implies, because context rot is real and frontier models are not deterministic at 400K tokens. But "matches the frontier model" is also the wrong success criterion. Match the ground truth, and measure both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would tell anyone building this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Recursion is not free.&lt;/strong&gt; On a benchmark where the context already fit the model's window, wrapping the same model in a recursive harness scored &lt;strong&gt;0.269 against 0.428&lt;/strong&gt; for a plain direct read. Recursion only pays when the context genuinely does not fit. Reach for it as a last resort, not a default. That comparison is from our OOLONG reproduction, and the paired run data is public: &lt;a href="https://huggingface.co/datasets/Rickesh/rlm-oolong-reproduction" rel="noopener noreferrer"&gt;huggingface.co/datasets/Rickesh/rlm-oolong-reproduction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent truncation is the dangerous failure.&lt;/strong&gt; Our serving layer auto-sized context to the prompt up to a ceiling, then quietly fell back: 30,021 tokens passed intact, 50,000 and 70,000 both clipped to exactly 16,387, with no error and a confident answer from the fragment. Compare processed-token counts against what you sent, and make the mismatch fatal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Substring scoring manufactures success.&lt;/strong&gt; One run reported correct because the model dumped raw corpus rows and the dump happened to contain the gold label. Require the declared answer form and reject anything that looks like regurgitated input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report what you could not parse.&lt;/strong&gt; &lt;code&gt;unparsed_lines=3&lt;/code&gt; is the number that makes the rest of the output trustworthy. A harness that silently drops what it cannot read will happily report a clean answer over half the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A partial sweep must refuse to answer.&lt;/strong&gt; If any segment fails, every key is undercounted. Ours exits non-zero rather than printing a number that looks fine.&lt;/p&gt;

&lt;p&gt;The engine is C++17 with zero third-party dependencies, and the planning and reduction stages are covered by 61 tests that need no GPU, no network and no tokens — because once the model is only doing extraction, everything else is ordinary code you can actually test.&lt;/p&gt;

</description>
      <category>privateai</category>
      <category>llm</category>
      <category>inference</category>
      <category>localllm</category>
    </item>
    <item>
      <title>99% token accuracy, zero learning. Field notes from fine-tuning vision models with RL.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:33:17 +0000</pubDate>
      <link>https://dev.to/rickeshtn/99-token-accuracy-zero-learning-field-notes-from-fine-tuning-vision-models-with-rl-306l</link>
      <guid>https://dev.to/rickeshtn/99-token-accuracy-zero-learning-field-notes-from-fine-tuning-vision-models-with-rl-306l</guid>
      <description>&lt;p&gt;Over the past year I have been fine-tuning open vision-language models - 9B dense up to a 35B mixture-of-experts - with supervised fine-tuning and GRPO-style reinforcement learning on verifiable rewards. Most of what I learned was not about algorithms. It was about the ways a training run can look healthy while doing nothing, or crash for reasons that have nothing to do with your code.&lt;/p&gt;

&lt;p&gt;Three failures, in increasing order of how long they fooled me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 1: the metric that measured the wrong thing (18 hours)
&lt;/h2&gt;

&lt;p&gt;I ran an 18-hour supervised fine-tune that reported token accuracy climbing steadily to 99%. Looked like a textbook run. The real evaluation metric - accuracy on multiple-choice questions - never moved.&lt;/p&gt;

&lt;p&gt;The cause was a mismatch between what I supervised and what I evaluated. The training loss was over free-text reasoning traces; the evaluation scored a single extracted answer letter. The model got extremely good at reproducing the &lt;em&gt;shape&lt;/em&gt; of the training text - hence 99% token accuracy - without that transferring to the decision I actually cared about.&lt;/p&gt;

&lt;p&gt;Token accuracy is a proxy, and proxies drift from the target exactly when you stop checking. The fix was structural, not a hyperparameter: supervise the thing you evaluate. If the deliverable is a constrained answer, the training signal has to reach that answer, not just the prose around it.&lt;/p&gt;

&lt;p&gt;The general rule I took: &lt;strong&gt;any training metric that is not your evaluation metric is a hypothesis about correlation, and you should check that correlation before you spend GPU-days on it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 2: the crash that was two libraries disagreeing about position ids
&lt;/h2&gt;

&lt;p&gt;The GRPO trainer for the 9B vision model crashed in the forward pass, deep inside rotary position embedding code. Nothing in my training code had changed.&lt;/p&gt;

&lt;p&gt;The diagnosis took a while because the bug lived at the boundary between components: the text sequence length was derived from token-type ids, while the vision sequence length came from the image grid - and image-pad tokens ended up counted twice. Two parts of the same stack, each internally consistent, disagreeing about how long the input was.&lt;/p&gt;

&lt;p&gt;For the 35B MoE variant of the same family, an equivalent rope bug was fixable by monkeypatching the model's position-id computation. I shipped the patch with a &lt;strong&gt;GPU-free regression test&lt;/strong&gt;: a tiny script that constructs the exact failing input shape and runs just the position-id path on CPU. It runs in seconds, needs no cluster, and fails loudly if an upstream update reintroduces the bug.&lt;/p&gt;

&lt;p&gt;Two lessons. First, when you fine-tune at the edge of a model family's tooling support, the bugs you hit are integration bugs, and the stack trace points at the victim, not the culprit. Second, every monkeypatch deserves a regression test that costs nothing to run - otherwise the next library upgrade silently un-fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 3: the RL loop that was learning the opposite (the quiet one)
&lt;/h2&gt;

&lt;p&gt;In a separate project I fine-tune a 9B model with reinforcement learning where the reward comes from realized real-world outcomes rather than a labelled dataset. For a long stretch the training signal was flat - not diverging, not collapsing, just flat, which is the least informative failure there is.&lt;/p&gt;

&lt;p&gt;Two compounding problems. One was label noise in the reward pipeline: some outcomes were being attributed to the wrong decisions, which dilutes any gradient. The other was worse: a sign error meant part of the advantage signal was inverted. The model was being gently pushed &lt;em&gt;away&lt;/em&gt; from behaviour that had worked.&lt;/p&gt;

&lt;p&gt;Nothing crashed. Every batch processed. Every log line looked like a training run. The only symptom was the absence of learning, and the only way I found it was working backwards from "the held-out metric should have moved by now" to auditing every stage of the reward computation by hand.&lt;/p&gt;

&lt;p&gt;After both fixes I got the first genuinely monotonic learning curve on that task. I still treat it as training signal only - the decider is held-out evaluation against the base model, and I do not report improvements that exist only in the training curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness rules I now run everything under
&lt;/h2&gt;

&lt;p&gt;These came out of the failures above plus a benchmarking programme across 70+ vision-language models. They are boring, and they are the difference between numbers and noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smoke test before committing compute.&lt;/strong&gt; A five-step GRPO run with two numbers watched: the PPO-style clip ratio and the fraction of outputs that parse. If the clip ratio is degenerate or parseability is low, the full run will be garbage in a way five steps already reveals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runs are gated fail-closed.&lt;/strong&gt; Nothing publishes a result unless the evaluation stage actually scored. "The eval crashed but training finished" is not a result; it is an unscored run, and unscored runs must be impossible to mistake for scored ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure failures and poor performance are different columns.&lt;/strong&gt; An unparseable output, an OOM, a crashed kernel - these are exceptions whose count must be exactly zero. A weak model produces zero exceptions and simply scores badly. If a quality threshold can absorb an infrastructure failure, a totally broken run can pass your gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The held-out metric is the only decider.&lt;/strong&gt; Training curves, token accuracy, reward trends - all of it is telemetry. If the held-out number did not move, nothing happened.&lt;/p&gt;

&lt;p&gt;None of this is novel. All of it is the difference between the runs I trust and the 18 hours I lost.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about ML evaluation, world models, and the ways measurement quietly fails. More at &lt;a href="https://dev.to/rickeshtn"&gt;dev.to/rickeshtn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>llm</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Your multi-agent system isn't hitting prompt cache. Your system prompt is the reason.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:49:39 +0000</pubDate>
      <link>https://dev.to/rickeshtn/your-multi-agent-system-isnt-hitting-prompt-cache-your-system-prompt-is-the-reason-4gb2</link>
      <guid>https://dev.to/rickeshtn/your-multi-agent-system-isnt-hitting-prompt-cache-your-system-prompt-is-the-reason-4gb2</guid>
      <description>&lt;p&gt;I run a multi-agent setup where ten agents analyse the same input. Same document, same market data, same everything. The only difference between them is persona: each one is instructed to look at the material through a different lens.&lt;/p&gt;

&lt;p&gt;Ten agents, one shared context. That should be the ideal case for prompt caching. Send the expensive context once, pay full price for it once, and let the other nine reads come back at a fraction of the cost.&lt;/p&gt;

&lt;p&gt;My cache hit rate was zero percent on three of the five models I was using, and under seven percent on the other two.&lt;/p&gt;

&lt;p&gt;I had been reading the bill for a while and optimising the wrong thing. Here is what was actually happening, because the mistake is structural and I doubt I am the only one making it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What prompt caching actually matches on
&lt;/h2&gt;

&lt;p&gt;Hosted inference providers cache on a &lt;strong&gt;prefix&lt;/strong&gt;. The provider hashes your request from the first token forward and looks for the longest run it has already computed. If your request starts with the same 3,000 tokens as a recent one, those 3,000 tokens are a cache read, typically around five times cheaper than a fresh read. The moment the token stream diverges, caching stops for the rest of the request. There is no re-syncing later.&lt;/p&gt;

&lt;p&gt;That word — prefix — is doing all the work, and I had not thought about it carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that broke it
&lt;/h2&gt;

&lt;p&gt;My call looked like every example in every SDK doc:&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;messages&lt;/span&gt; &lt;span class="o"&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;system&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;agent_persona&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# differs per agent
&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;shared_context&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# identical for all 10
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The persona is short. A couple of hundred tokens describing how this particular agent should reason. The shared context is large: several thousand tokens of source material.&lt;/p&gt;

&lt;p&gt;Read that message array as a flat token stream, which is what the provider does. The first thing in the stream is the persona. The persona is &lt;strong&gt;different for every agent&lt;/strong&gt;. So the prefix diverges at roughly token one, and the several thousand tokens of identical context sitting behind it can never match anything.&lt;/p&gt;

&lt;p&gt;Ten agents. Ten identical copies of the same context. Ten full-price reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it rather than assuming it
&lt;/h2&gt;

&lt;p&gt;I did not want to guess, so I hashed both halves of every call for a single work item and counted the distinct values.&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;SELECT&lt;/span&gt;
  &lt;span class="nc"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prompt_sha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distinct_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nc"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;system_sha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distinct_system&lt;/span&gt;
&lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;agent_calls&lt;/span&gt;
&lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distinct_user   = 1
distinct_system = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One user prompt. Ten system prompts. The expensive half was &lt;strong&gt;byte-identical across all ten calls&lt;/strong&gt;, and the cheap half in front of it was unique every time.&lt;/p&gt;

&lt;p&gt;This lines up exactly with the provider's own usage report, which broke my spend into cached and uncached input tokens:&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;cached share of input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;11.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those low non-zero numbers are incidental collisions between unrelated calls, not the structural reuse I should have been getting. If the design were right, nine out of every ten context reads would be cache hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Put the shared, expensive, identical part first. Put the small, varying part last.&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;messages&lt;/span&gt; &lt;span class="o"&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;system&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;shared_context&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;          &lt;span class="c1"&gt;# identical -&amp;gt; caches
&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="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;agent_persona&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&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="c1"&gt;# varies, small, last
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the first several thousand tokens are the same for all ten agents. The first agent pays full price and warms the cache. The other nine read it back at cache rates. The only uncached part is the couple of hundred persona tokens at the tail, which is what you actually want to be paying for.&lt;/p&gt;

&lt;p&gt;The general rule, which I now think should be a design constraint rather than an optimisation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Order your prompt from &lt;strong&gt;most shared&lt;/strong&gt; to &lt;strong&gt;most specific&lt;/strong&gt;. Caching rewards a stable prefix, and every byte that varies early poisons everything after it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This also composes with how you batch. If you run the same model across many items back to back, you keep hitting a warm prefix. If you round-robin across models for each item, you cold-start the cache on every single call. Grouping by model, not by work item, keeps the cache warm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I am not comfortable with
&lt;/h2&gt;

&lt;p&gt;Moving the persona out of &lt;code&gt;system&lt;/code&gt; and into &lt;code&gt;user&lt;/code&gt; is not free.&lt;/p&gt;

&lt;p&gt;Some models weight system instructions more strongly than user content. That is often the point of a system prompt. If one of my agents is specifically instructed to argue an unpopular position, and I demote that instruction from system to user, it may hedge more. I would be trading spend for behaviour, and I would not necessarily notice, because the output would still be well-formed and plausible.&lt;/p&gt;

&lt;p&gt;So this is not a change I would ship straight to production off the back of a cost argument. It needs an A/B on a sample of items, comparing the actual decisions each layout produces, not just checking that the responses parse.&lt;/p&gt;

&lt;p&gt;There is a middle path worth trying first: keep a short stable instruction in &lt;code&gt;system&lt;/code&gt; that is &lt;strong&gt;identical across all agents&lt;/strong&gt;, and move only the per-agent differentiation into the user message. You get a shared prefix and keep a system-role framing. Whether that is enough depends on how much of your agents' behaviour hangs off the system role, which is an empirical question about your prompts and your models.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefix means prefix.&lt;/strong&gt; Anything that varies early destroys caching for everything after it, no matter how much identical material follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument it.&lt;/strong&gt; Hash the components of your requests and count distinct values per work item. It took one query to turn a vague suspicion into a definite structural bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the cached-versus-uncached split in your usage report.&lt;/strong&gt; A near-zero cache rate on a workload with obvious shared context is not a pricing quirk. It is a design bug, and it is telling you the prefix is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The default SDK message shape is not cache-aware.&lt;/strong&gt; Persona-in-system, content-in-user is the shape in every tutorial. It is exactly wrong for fan-out workloads where many personas share one context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I had spent real effort choosing cheaper models before I checked whether I was paying for the same tokens ten times over. The model swap was worth doing. It was also the second-biggest lever, and I found it first because it was the one I was looking for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Addendum: what the comments corrected
&lt;/h2&gt;

&lt;p&gt;This post got better feedback than it deserved, and three points are important enough to belong in the body rather than below it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is a third option I missed, and it is the right one where available.&lt;/strong&gt; &lt;a href="https://dev.to/skillselion"&gt;@skillselion&lt;/a&gt; pointed out that on APIs with explicit cache breakpoints, the system prompt can be an array of content blocks: shared context as the first block carrying a &lt;code&gt;cache_control&lt;/code&gt; marker, persona as a second block after it. The cached prefix ends at the marker, so every agent hits the same cache while the persona keeps its system-role framing. That dissolves the trade-off I spent a section being uncomfortable about. The role-versus-cost tension is an artifact of &lt;em&gt;implicit&lt;/em&gt; prefix caching, not something inherent to the problem. Where the provider gives you breakpoints, use them; my middle path is for providers that only do implicit prefix matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Put the shared context first" is necessary but not sufficient.&lt;/strong&gt; &lt;a href="https://dev.to/max_quimby"&gt;@max_quimby&lt;/a&gt; and &lt;a href="https://dev.to/kartik-nvjk"&gt;@kartik-nvjk&lt;/a&gt; both made the same point: the shared prefix has to be &lt;em&gt;byte-identical&lt;/em&gt; across calls. A per-run request id, a timestamp, or a reordered JSON key injected anywhere before the boundary resets the prefix, and you are back to zero with a layout that looks correct. Enforce a canonical serialization for the cached block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prefix starts earlier than your message array.&lt;/strong&gt; &lt;a href="https://dev.to/mads_hansen_27b33ebfee4c9"&gt;@mads_hansen&lt;/a&gt; noted that "identical text" is not the same as an identical provider prefix — tool schemas, response formats, model ids and SDK serialization defaults can all sit ahead of or inside your visible messages. Worth confirming against your provider's documented render order; on Anthropic's API, for instance, it is tools, then system, then messages, which means an unstable tool list invalidates the cache before your system prompt is even reached. Hash the canonical request envelope, not just the parts you wrote by hand.&lt;/p&gt;

&lt;p&gt;Mads also proposed a three-layer layout I have adopted: stable policy shared by every agent, stable source context shared by the work item, then the small role and question suffix — each versioned separately, so a persona change invalidates only the cheap tail while a policy change invalidates the shared prefix deliberately. That turns cache behaviour into an observable property of the prompt architecture instead of a billing surprise.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>Can a Cheap Model Beat a Frontier Model? Rebuilding Recursive Language Models with Codex</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Sun, 09 Aug 2026 15:05:19 +0000</pubDate>
      <link>https://dev.to/rickeshtn/can-a-cheap-model-beat-a-frontier-model-rebuilding-recursive-language-models-with-codex-2m45</link>
      <guid>https://dev.to/rickeshtn/can-a-cheap-model-beat-a-frontier-model-rebuilding-recursive-language-models-with-codex-2m45</guid>
      <description>&lt;p&gt;Large language models have enormous context windows now. That does not mean they use all of that context reliably.&lt;/p&gt;

&lt;p&gt;As prompts grow, models can miss details, lose track of relationships, or produce plausible summaries instead of doing the exhaustive work a question requires. The Recursive Language Models (RLM) paper proposes a different interface: keep the large context outside the model, expose it as a variable in a persistent programming environment, and let the model inspect, partition, and recursively query smaller pieces.&lt;/p&gt;

&lt;p&gt;We rebuilt that method with an unusual constraint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Codex CLI as the model backend;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpt-5.4-mini&lt;/code&gt; for both the RLM root and every subcall;&lt;/li&gt;
&lt;li&gt;a direct frontier model only as a separate baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was encouraging, expensive, and more nuanced than “cheap model equals frontier model.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What an RLM changes
&lt;/h2&gt;

&lt;p&gt;A normal model call looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;large prompt -&amp;gt; model -&amp;gt; answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An RLM instead gives the root model metadata about the input and a Python REPL containing the real context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;question
   |
root model
   |
persistent REPL holding the context
   |-- inspect and search with code
   |-- split context into useful chunks
   |-- call smaller LMs over those chunks
   |-- validate and aggregate results
   `-- return the final answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is that the root model does not need to carry every document, record, tool result, and partial answer in its own context window. Large intermediate values can remain in REPL variables. Subcalls receive focused, locally understandable tasks.&lt;/p&gt;

&lt;p&gt;That makes RLM less like a bigger prompt and more like an out-of-core data-processing system whose semantic operator happens to be a language model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually tested
&lt;/h2&gt;

&lt;p&gt;We used an OOLONG &lt;code&gt;trec_coarse&lt;/code&gt; validation example from the protocol described in the RLM work.&lt;/p&gt;

&lt;p&gt;The input was a 308,367-character context containing 3,182 general-knowledge questions. Each question implicitly belonged to one of six answer types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;numeric value&lt;/li&gt;
&lt;li&gt;entity&lt;/li&gt;
&lt;li&gt;human being&lt;/li&gt;
&lt;li&gt;location&lt;/li&gt;
&lt;li&gt;abbreviation&lt;/li&gt;
&lt;li&gt;description and abstract concept&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The labels were not present in the context. The task was to infer the labels and identify the least-common category.&lt;/p&gt;

&lt;p&gt;We compared:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A direct &lt;code&gt;gpt-5.6-sol&lt;/code&gt; Codex call.&lt;/li&gt;
&lt;li&gt;An RLM where the root and all leaf calls were locked to &lt;code&gt;gpt-5.4-mini&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The direct frontier call answered &lt;code&gt;abbreviation&lt;/code&gt; and scored zero. The mini-only RLM answered &lt;code&gt;numeric value&lt;/code&gt;, matching the gold answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Model calls&lt;/th&gt;
&lt;th&gt;Elapsed time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct frontier call&lt;/td&gt;
&lt;td&gt;Incorrect&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;40.1 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLM with &lt;code&gt;gpt-5.4-mini&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;At least 238&lt;/td&gt;
&lt;td&gt;6,120.3 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RLM root first inspected the structure of the context. It then classified chunks, retried malformed responses, reduced the chunk size, reclassified all 3,182 questions using structured JSON outputs, checked that it had coverage, and calculated the minimum.&lt;/p&gt;

&lt;p&gt;This is exactly the sort of work that a direct model call often approximates but a recursive program can force itself to perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable but useful audit
&lt;/h2&gt;

&lt;p&gt;Getting the final answer right did not mean every intermediate judgment was right.&lt;/p&gt;

&lt;p&gt;We compared the mini model's inferred counts against the validated labels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;True count&lt;/th&gt;
&lt;th&gt;Mini inferred&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numeric value&lt;/td&gt;
&lt;td&gt;398&lt;/td&gt;
&lt;td&gt;402&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity&lt;/td&gt;
&lt;td&gt;521&lt;/td&gt;
&lt;td&gt;623&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human being&lt;/td&gt;
&lt;td&gt;544&lt;/td&gt;
&lt;td&gt;488&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location&lt;/td&gt;
&lt;td&gt;571&lt;/td&gt;
&lt;td&gt;493&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abbreviation&lt;/td&gt;
&lt;td&gt;571&lt;/td&gt;
&lt;td&gt;560&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description and abstract concept&lt;/td&gt;
&lt;td&gt;577&lt;/td&gt;
&lt;td&gt;616&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model made substantial row-level classification errors. It still found the correct minimum because numeric value had a 123-item margin over the next-smallest true category.&lt;/p&gt;

&lt;p&gt;That distinction matters. This run shows that decomposition changed the outcome and allowed a cheap model to solve one problem that the direct frontier call missed. It does not prove that the cheap model reconstructed the data exactly, and one row does not establish general equality between the two systems.&lt;/p&gt;

&lt;p&gt;The honest claim is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On suitable long-context tasks, a cheap model inside an RLM can match or outperform a direct frontier-model call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Use cases already demonstrated by RLM research
&lt;/h2&gt;

&lt;p&gt;The paper evaluates four useful task shapes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Semantic aggregation
&lt;/h3&gt;

&lt;p&gt;OOLONG requires labeling and aggregating information spread throughout a large input. Real applications include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer-feedback analysis;&lt;/li&gt;
&lt;li&gt;support-ticket taxonomies;&lt;/li&gt;
&lt;li&gt;survey aggregation;&lt;/li&gt;
&lt;li&gt;incident and application-log analysis;&lt;/li&gt;
&lt;li&gt;quality-control statistics over text records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our experiment belongs to this category.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Multi-document research
&lt;/h3&gt;

&lt;p&gt;BrowseComp-Plus requires joining evidence across documents in a very large offline corpus. Analogous applications include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;literature reviews;&lt;/li&gt;
&lt;li&gt;technical-documentation research;&lt;/li&gt;
&lt;li&gt;contract and policy comparison;&lt;/li&gt;
&lt;li&gt;due-diligence document rooms;&lt;/li&gt;
&lt;li&gt;evidence-backed competitive research.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Repository-scale understanding
&lt;/h3&gt;

&lt;p&gt;The paper includes LongBench-v2 CodeQA, where questions require reasoning across files in a codebase. Probable uses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;architecture mapping;&lt;/li&gt;
&lt;li&gt;migration-impact analysis;&lt;/li&gt;
&lt;li&gt;dependency and license audits;&lt;/li&gt;
&lt;li&gt;security triage;&lt;/li&gt;
&lt;li&gt;locating missing tests;&lt;/li&gt;
&lt;li&gt;comparing implementation against documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Cross-record and pairwise reasoning
&lt;/h3&gt;

&lt;p&gt;OOLONG-Pairs asks the system to construct relationships between combinations of records. Applications could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;entity resolution;&lt;/li&gt;
&lt;li&gt;policy-conflict detection;&lt;/li&gt;
&lt;li&gt;matching candidates against constraints;&lt;/li&gt;
&lt;li&gt;finding related incidents;&lt;/li&gt;
&lt;li&gt;identifying incompatible configurations;&lt;/li&gt;
&lt;li&gt;relationship discovery across an archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These workloads can grow quadratically, so they need strict budgets and deterministic post-processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical new use case: making sense of agent-session archives
&lt;/h2&gt;

&lt;p&gt;While exploring our local Claude Code history, we found a single session transcript that was 242 MB and contained 39,570 JSONL records. All project transcripts together occupied about 3.6 GB.&lt;/p&gt;

&lt;p&gt;The large session was not 242 MB of useful conversation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;about 176 MB was attachment records;&lt;/li&gt;
&lt;li&gt;about 29 MB was assistant events;&lt;/li&gt;
&lt;li&gt;about 20 MB was user and tool-result events;&lt;/li&gt;
&lt;li&gt;about 12 MB was file-history snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is an excellent RLM-shaped problem.&lt;/p&gt;

&lt;p&gt;A deterministic first pass can stream the JSONL, hash duplicate attachments, reconstruct parent-child event relationships, merge subagent logs, and extract messages, commands, file changes, tests, commits, errors, and outcomes. An RLM can then analyze normalized episodes and recursively build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a cross-session project timeline;&lt;/li&gt;
&lt;li&gt;a decision register;&lt;/li&gt;
&lt;li&gt;a map of attempted and abandoned approaches;&lt;/li&gt;
&lt;li&gt;recurring failure patterns;&lt;/li&gt;
&lt;li&gt;unresolved tasks;&lt;/li&gt;
&lt;li&gt;evidence-linked summaries of what actually shipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final report should cite session IDs, event IDs, timestamps, commands, and Git commits. Otherwise, it is merely another plausible summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other probable use cases
&lt;/h2&gt;

&lt;p&gt;The same decomposition pattern should transfer to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long incident timelines assembled from logs, tickets, and chat;&lt;/li&gt;
&lt;li&gt;scientific evidence extraction across papers and experiment records;&lt;/li&gt;
&lt;li&gt;compliance control-to-evidence mapping;&lt;/li&gt;
&lt;li&gt;large archives of meetings, email, or project documents;&lt;/li&gt;
&lt;li&gt;ranking records against a nuanced rubric;&lt;/li&gt;
&lt;li&gt;graph filtering and multi-hop relationship discovery;&lt;/li&gt;
&lt;li&gt;reconciling conflicting claims across many sources;&lt;/li&gt;
&lt;li&gt;constructing structured datasets from heterogeneous text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recurring requirement is not simply “the input is long.” A good RLM task has four properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The context can be partitioned or searched programmatically.&lt;/li&gt;
&lt;li&gt;Smaller semantic subtasks remain understandable to the cheap model.&lt;/li&gt;
&lt;li&gt;Intermediate results can be stored in a structured form.&lt;/li&gt;
&lt;li&gt;The final result can be verified or recomputed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where RLM is probably the wrong tool
&lt;/h2&gt;

&lt;p&gt;RLM is a poor default for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-latency chat;&lt;/li&gt;
&lt;li&gt;simple questions that fit comfortably in one prompt;&lt;/li&gt;
&lt;li&gt;sparse retrieval where grep or conventional search is sufficient;&lt;/li&gt;
&lt;li&gt;creative writing that depends on a single coherent voice;&lt;/li&gt;
&lt;li&gt;exact high-stakes decisions without an independent verifier;&lt;/li&gt;
&lt;li&gt;public execution of untrusted model-generated Python;&lt;/li&gt;
&lt;li&gt;high-volume synchronous APIs with tight latency budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our successful row took roughly 102 minutes. That is acceptable for a research run or an overnight audit, not for an interactive endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a reusable package should look like
&lt;/h2&gt;

&lt;p&gt;The useful abstraction is not an OOLONG runner and not one universal prompt. It is a context-compute runtime with a small set of reusable recipes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run(
  context,
  objective,
  recipe,
  answer_schema,
  verifier,
  budget
) -&amp;gt; answer + evidence + validation + trajectory + usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initial recipes could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aggregate_records&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evidence_synthesis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repository_analysis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cross_record_join&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;candidate_ranking&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our intended configuration, the Codex backend would keep both root and subcalls locked to &lt;code&gt;gpt-5.4-mini&lt;/code&gt;. A frontier model would appear only in evaluation runs, never inside the RLM call tree.&lt;/p&gt;

&lt;p&gt;Production use would also require an isolated execution environment, call and token limits, schema validation, redaction, prompt-injection defenses, resumable runs, and source-level evidence for every important claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The one-row result is a proof of mechanism, not a benchmark victory.&lt;/p&gt;

&lt;p&gt;The immediate research questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the advantage survive across all 50 paired OOLONG tasks?&lt;/li&gt;
&lt;li&gt;Can concurrency reduce the 102-minute runtime without changing quality?&lt;/li&gt;
&lt;li&gt;Which decomposition recipes transfer cleanly between domains?&lt;/li&gt;
&lt;li&gt;How much verification is required for exact row-level work?&lt;/li&gt;
&lt;li&gt;Can a mini-only RLM turn multi-gigabyte agent histories into a reliable, source-linked development narrative?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RLMs do not magically turn a cheap model into a frontier model. They change the computation available to that model. Sometimes that difference is enough to turn a wrong one-shot answer into a correct, auditable process.&lt;/p&gt;

&lt;p&gt;That is a more interesting result than the slogan.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reproduction data: the paired OOLONG runs behind these numbers are published at &lt;a href="https://huggingface.co/datasets/Rickesh/rlm-oolong-reproduction" rel="noopener noreferrer"&gt;huggingface.co/datasets/Rickesh/rlm-oolong-reproduction&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>We made our world model smaller and it got better. Then "efficient" attention made nothing faster.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:56:04 +0000</pubDate>
      <link>https://dev.to/rickeshtn/we-made-our-world-model-smaller-and-it-got-better-then-efficient-attention-made-nothing-faster-18f6</link>
      <guid>https://dev.to/rickeshtn/we-made-our-world-model-smaller-and-it-got-better-then-efficient-attention-made-nothing-faster-18f6</guid>
      <description>&lt;p&gt;Two experiments on the same world model, both of which came out the opposite way to how I expected. One is now an IJCNN 2026 paper, the other an ICPR 2026 paper. Both are really about the same thing: the scaling intuitions most of us carry around are older than the hardware we run on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a world model has to do here
&lt;/h2&gt;

&lt;p&gt;A dynamic scene reconstruction model takes sparse multi-view camera observations across time and predicts a full 3D scene, including how things are moving. Point a few cameras at a street for two seconds, get back a 3D representation of that street with the cars in it moving correctly.&lt;/p&gt;

&lt;p&gt;This is the perception half of a world model for driving and robotics. If you want to simulate "what happens if I turn left here," you first need a model that can build the scene at all.&lt;/p&gt;

&lt;p&gt;The architecture we worked from is &lt;a href="https://arxiv.org/abs/2501.00602" rel="noopener noreferrer"&gt;STORM&lt;/a&gt; (Yang et al.), a transformer that predicts 3D Gaussian primitives and their motion in a single forward pass. Feed-forward, so no per-scene optimization: a big deal if you ever want this on a vehicle rather than in an offline pipeline. That's prior work, not ours. What follows is what we found building on top of it.&lt;/p&gt;

&lt;p&gt;Setup throughout: a subset of the Waymo Open Dataset, 2-second clips at 10 fps, up to three synchronized camera views, images downscaled to 160x240, 150K-300K training iterations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 1: we allocated the parameters differently and the smaller model won
&lt;/h2&gt;

&lt;p&gt;The default move when a transformer underperforms is to make it deeper. More layers, more capacity, better results. That instinct comes from language modelling and image classification, and it is mostly right there.&lt;/p&gt;

&lt;p&gt;Dense spatio-temporal prediction is not those tasks.&lt;/p&gt;

&lt;p&gt;We built a width-dominant, shallow variant. Concretely: fewer transformer layers, larger embedding dimension, and fewer attention heads each with a bigger head dimension. Then we ran it in a &lt;em&gt;smaller&lt;/em&gt; configuration against the standard baseline in a larger one, under an identical training and data pipeline.&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;PSNR&lt;/th&gt;
&lt;th&gt;SSIM&lt;/th&gt;
&lt;th&gt;Depth RMSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STORM baseline (B8)&lt;/td&gt;
&lt;td&gt;26.82&lt;/td&gt;
&lt;td&gt;0.770&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.59&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ours, width-dominant (V6, S8 config)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27.57&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.806&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15.59&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;+0.75 PSNR and +0.036 SSIM, from the smaller model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The training dynamics were the more interesting part. The width-dominant model converged faster, to a higher plateau, along a visibly smoother trajectory, and its gradient norms were substantially more stable. It was not squeezing out a marginal win at the end of a hard optimization. It was simply an easier model to optimize.&lt;/p&gt;

&lt;p&gt;Why this is plausible: self-attention already provides global mixing within every single layer. Stacking more layers buys you more sequential refinement, but sequential depth is exactly what makes optimization harder. Widening instead gives each attention head a richer representation to work with while leaving global information exchange intact. For a task where every token needs to talk to every other token anyway, spending your parameter budget on width rather than depth is not obviously the wrong call, and here it was the right one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now the part I want to be honest about&lt;/strong&gt;: look at the depth RMSE column. The baseline is clearly better on geometry, 12.59 against our 15.59. We got prettier images and worse depth.&lt;/p&gt;

&lt;p&gt;That trade-off is real and we reported it as a finding rather than burying it. Photometric quality and geometric accuracy are not the same axis, and if you are building a world model whose output feeds a planner rather than a display, the column we lost on may be the column you actually care about. Anyone reading "+0.75 PSNR" as an unqualified win has read the paper badly.&lt;/p&gt;

&lt;p&gt;The practical upshot is deployability. A smaller model that trains on consumer GPUs and beats a larger one on photometric quality changes who can work on this at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 2: attention reuse, and a result I did not want
&lt;/h2&gt;

&lt;p&gt;Self-attention costs O(N² · d_h · H). For spatio-temporal input the token count N is brutal, because you are multiplying pixels by views by timesteps. So the obvious efficiency target is attention itself.&lt;/p&gt;

&lt;p&gt;There is a neat line of work on &lt;strong&gt;attention reuse&lt;/strong&gt;, built on the observation that attention maps stabilize as you go deeper into a transformer. If layer 9's attention pattern looks a lot like layer 8's, why recompute the query-key interaction at all? Reuse and transform the previous map instead. You keep the global token interaction and drop a chunk of the compute.&lt;/p&gt;

&lt;p&gt;We integrated Less-Attention layers into the STORM-B/8 backbone and evaluated across a range of reuse ratios, measuring runtime, memory, parameter growth and reconstruction quality, under both naive and optimized implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attention reuse did not improve wall-clock time over optimized full attention. At any reuse ratio. Even with careful implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason is not that the theory is wrong. The FLOPs really do go away. The reason is that fused attention kernels on modern GPUs have already moved the bottleneck somewhere else. When full attention runs as a single fused kernel that never materializes the attention matrix in HBM, the thing you were paying for was mostly memory traffic, not arithmetic — and "skip some of the arithmetic" optimizes the wrong resource. You also add parameters and complexity for the reuse machinery, and you now have two kernels where the fused path had one.&lt;/p&gt;

&lt;p&gt;This is the whole lesson: &lt;strong&gt;an algorithmic optimization derived from a FLOP count is a hypothesis about the hardware, not a fact about it.&lt;/strong&gt; The hardware moved. FlashAttention-style kernels changed which operations are expensive, and a good chunk of the "efficient attention" literature was implicitly costed against a machine that no longer exists.&lt;/p&gt;

&lt;p&gt;I would rather this had worked. Publishing a null result is less fun than publishing a speedup. But a null result that saves other people from re-implementing the same thing is worth more than another 3% on a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from both
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Capacity allocation matters more than total capacity.&lt;/strong&gt; Where you put the parameters is a real design decision with real consequences, and "make it bigger" is the answer you give when you have not measured which axis is actually binding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure wall-clock on your target hardware. Always.&lt;/strong&gt; Not FLOPs, not parameter counts, not asymptotic complexity. Those are proxies, and proxies drift as hardware evolves. Every efficiency claim has a silent "on the machine I tested" attached to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report the column you lost on.&lt;/strong&gt; Our depth RMSE got worse. Someone building on this needs to know that far more than they need another decimal place of PSNR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The intuitions are load-bearing and mostly untested.&lt;/strong&gt; "Deeper is better" and "fewer FLOPs is faster" are both reasonable priors that happened to be wrong in our setting. I only found that out by running the ablation instead of assuming.&lt;/p&gt;

&lt;p&gt;If you work on neural rendering, world models, or transformer efficiency, I would genuinely like to hear whether the width-over-depth result holds in your setting. My prior is that it generalizes to dense prediction tasks and not much further, but that is a prior, and this post is largely about how those go.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Papers: "Rethinking Transformer Design for Dynamic Scene Reconstruction: An Efficient, Width-Dominant Approach" (IJCNN 2026) and "Evaluating Attention Reuse in Dynamic 3D Gaussian Reconstruction" (ICPR 2026).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write about world models, robotics data pipelines, and the ways measurement quietly fails. &lt;a href="https://linkedin.com/in/rickeshnatarajan" rel="noopener noreferrer"&gt;linkedin.com/in/rickeshnatarajan&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>computervision</category>
      <category>ai</category>
      <category>performance</category>
    </item>
    <item>
      <title>Three Ways Your Training Data Lies to You (And None of Them Throw an Error)</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:21:27 +0000</pubDate>
      <link>https://dev.to/rickeshtn/three-ways-your-training-data-lies-to-you-and-none-of-them-throw-an-error-4044</link>
      <guid>https://dev.to/rickeshtn/three-ways-your-training-data-lies-to-you-and-none-of-them-throw-an-error-4044</guid>
      <description>&lt;p&gt;Every failure I am about to describe produced a clean run. No exception, no stack trace, no red build. Each one produced a plausible number that I believed for longer than I should have.&lt;/p&gt;

&lt;p&gt;That is the category of bug I have come to fear most. A crash tells you it crashed. A silently broken dataset tells you nothing at all, and your metrics will politely agree with it.&lt;/p&gt;

&lt;p&gt;Here are three from the last year, all from my own work, all found late.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The dataset that was 92% one category
&lt;/h2&gt;

&lt;p&gt;I had a training set of 688 records for a multi-category vision-language task. Thirteen categories. Reasonable size for a fine-tune, already used in a completed training run whose results I had written up.&lt;/p&gt;

&lt;p&gt;While preparing a stratified split, I joined the records back against the source annotations and actually counted the categories.&lt;/p&gt;

&lt;p&gt;630 of 688 were a single category: scene captions. Zero examples of traffic signals. Zero of planning. Zero of uncertainty. Several categories the evaluation explicitly measured had no representation in training at all.&lt;/p&gt;

&lt;p&gt;The previous fine-tune had shown gains on some of those very categories. I had interpreted this as the model learning the task. The real explanation was duller and more useful: the model had learned the &lt;em&gt;answer format&lt;/em&gt; from caption supervision, and format alignment alone was enough to move a multiple-choice score. Nothing category-specific had been learned, because nothing category-specific had been shown.&lt;/p&gt;

&lt;p&gt;The root cause was upstream and boring. The conversion script I inherited only rewrote file paths and dropped records with missing frames. It faithfully preserved a caption-only selection made further up the chain. It had no opinion about balance because nobody had asked it to have one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I changed:&lt;/strong&gt; the composition of a training set is now an artifact I generate and inspect before any run, not a property I assume. A category histogram takes seconds. I had not looked, for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The 18-hour run that converged perfectly to nothing
&lt;/h2&gt;

&lt;p&gt;Large model, QLoRA, multi-GPU, 8 camera views per sample. Roughly a full day of compute.&lt;/p&gt;

&lt;p&gt;The loss curve was beautiful. It fell from 19.4 to 15.7 over the first 26 steps, then to 0.078 by step 51, then flattened near 0.02 and stayed there. Token accuracy reached 0.99. Gradient norms decayed smoothly. Nothing in the training telemetry looked wrong.&lt;/p&gt;

&lt;p&gt;The task metric was 0.10.&lt;/p&gt;

&lt;p&gt;The cause: I was supervising on free-text answers, median 11 words, phrased like "One should keep to the right side of the road and drive slowly." The evaluation was four-option multiple choice, scored on the index of the chosen option.&lt;/p&gt;

&lt;p&gt;The model learned, quickly and correctly, to reproduce the &lt;em&gt;style&lt;/em&gt; of the training answers. That is genuinely what the loss was asking for, and it maxed it out in about 50 steps. It was never asked to select an option, so it never learned to.&lt;/p&gt;

&lt;p&gt;Training and evaluation were measuring different tasks. Both were internally consistent. Neither could detect the other's disagreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I changed:&lt;/strong&gt; before a long run starts, I now write down what the eval measures and what the loss optimizes, in one line each, and check they describe the same thing. When loss collapses to near zero within a few dozen steps, I treat that as an alarm rather than a success. Genuine learning on a hard task does not look like that.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The dataset that was 40% smaller than its file size claimed
&lt;/h2&gt;

&lt;p&gt;Assessing a large public robotics corpus for adoption. The headline: 3.91 TB compressed, 4.65 TB extracted, hundreds of shards, dozens of buildings, thousands of hours of teleoperated manipulation.&lt;/p&gt;

&lt;p&gt;The number that mattered was buried in the paper's method section. Only about 60% of the raw data converted into reliable 3D flow annotations. The rest failed depth estimation, camera pose optimization, or point tracking. Then a further filter kept only trajectories with actual robot-object contact and real object motion.&lt;/p&gt;

&lt;p&gt;Nominal size: thousands of hours. Training-ready size after both filters: roughly 500 hours.&lt;/p&gt;

&lt;p&gt;Every plan built on the first number was wrong by a factor of several. Storage estimates, download time, compute budget, and most importantly the question of whether the corpus was even large enough for what we wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I changed:&lt;/strong&gt; I now treat &lt;em&gt;yield&lt;/em&gt; as the primary dataset metric, not size. What fraction of raw capture survives every stage into training-ready data? For collected data this is a hard operational KPI, and it is the one that determines cost per usable hour. Bytes on disk is a storage figure. It tells you almost nothing about what you can train on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;All three share a shape.&lt;/p&gt;

&lt;p&gt;A pipeline stage did exactly what it was written to do. A metric moved in the direction that metric moves when things go well. And the thing I actually cared about was not being measured by anything at all.&lt;/p&gt;

&lt;p&gt;The defenses that work for me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check that work happened, not just that output exists.&lt;/strong&gt; A gate that verifies "400 rows were produced" while never checking "any row succeeded" will pass a completely dead run. I once had an evaluation return zero correct on every row for 45 minutes because a kernel was unavailable on that GPU. Zero is a valid ratio. Row counts agreed. The gate passed and the sweep moved on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count infrastructure failures separately, and require zero.&lt;/strong&gt; A weak model produces well-formed output and scores badly. A broken pipeline produces nothing and scores identically. Collapsing those two into one number destroys the only signal that distinguishes them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emit progress with errors first.&lt;/strong&gt; Anything running longer than a minute should print failure count before position before metric. Silence is not neutral. A job working perfectly and a job failing on every single item look exactly the same from outside when neither prints anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate splits as their own gated job.&lt;/strong&gt; I run split validation as a separate step that must pass before training is allowed to start. It checks class balance, domain distribution across sites, and leakage between splits. It has caught things that would otherwise have cost days of GPU time and, worse, produced a believable number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down what the metric would look like if the thing were broken.&lt;/strong&gt; If you cannot answer that, you cannot tell success from failure, and you will default to reading any completed run as a successful one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;In every one of these cases the data was the limiting factor, and in every case I spent the first stretch of debugging looking at the model.&lt;/p&gt;

&lt;p&gt;That instinct is hard to unlearn, because the model is where the interesting work feels like it lives. But architecture, optimizer, and learning rate are all things I can inspect in a config file in about a minute. The composition of the training set is something I have to deliberately go and measure, and that extra step is exactly why it goes unchecked.&lt;/p&gt;

&lt;p&gt;The failure mode is not carelessness. It is that broken data produces output that looks entirely normal.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on robotics data pipelines, model evaluation, and the operational side of making training data trustworthy. &lt;a href="https://linkedin.com/in/rickeshnatarajan" rel="noopener noreferrer"&gt;linkedin.com/in/rickeshnatarajan&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>mlops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your reasoning model isn't dumb. Your parser is throwing away its best answers.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:19:57 +0000</pubDate>
      <link>https://dev.to/rickeshtn/your-reasoning-model-isnt-dumb-your-parser-is-throwing-away-its-best-answers-4kdg</link>
      <guid>https://dev.to/rickeshtn/your-reasoning-model-isnt-dumb-your-parser-is-throwing-away-its-best-answers-4kdg</guid>
      <description>&lt;p&gt;I benchmarked a vision-language model and scored it at 0.31.&lt;/p&gt;

&lt;p&gt;The real number was 0.70. Same model, same weights, same hardware, same 100 questions. The only thing that changed was how I read its output.&lt;/p&gt;

&lt;p&gt;I had already written up the 0.31 as a capability finding and concluded the model was unsuitable. That conclusion was wrong, and the failure was entirely in my harness. Here is the mistake, because I doubt I am the only one making it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I was evaluating a batch of open-weight and frontier models on a multiple-choice benchmark: multi-view driving scenes, four options per question, one correct answer. Standard stuff. The prompt asked for reasoning followed by a final line, &lt;code&gt;Answer: X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My scoring code did the obvious thing:&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer:\s*([A-D])&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&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;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# None scores as wrong
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last comment is the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The model I was testing is a "thinking" model. It emits a long internal reasoning trace before it commits to an answer. I had a generation budget of 1024 tokens.&lt;/p&gt;

&lt;p&gt;On easy questions it reasoned briefly, emitted &lt;code&gt;Answer: B&lt;/code&gt;, and scored fine. On hard questions it reasoned at length, hit the token cap mid-thought, and never emitted the answer line at all.&lt;/p&gt;

&lt;p&gt;So the harness scored every one of those as wrong.&lt;/p&gt;

&lt;p&gt;64 of 100 questions returned no parseable answer. Zero of those were image-loading errors or crashes. They were all truncation. And the truncation was not random:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncertainty                 0/8   answered
Counterfactual              0/3   answered
Safety-critical Planning    1/11  answered
Safety-critical Prediction  3/12  answered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that distribution. The questions the model failed to answer were precisely the questions that required the most reasoning. My harness was systematically discarding the model's performance on exactly the hard subset I was trying to measure, and reporting the result as a capability ceiling.&lt;/p&gt;

&lt;p&gt;Of the 36 it did answer, it got 86% right. The model was fine. My measurement was garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Stop parsing free text. Constrain the decoding to a schema.&lt;/p&gt;

&lt;p&gt;With Ollama:&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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&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;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;my-vlm:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&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;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;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgs&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;# enforced at decode time
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;object&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;properties&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;answer&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;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;string&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;enum&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;A&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;B&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;C&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;D&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;required&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;answer&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="n"&gt;think&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The equivalent exists nearly everywhere now: &lt;code&gt;response_format&lt;/code&gt; with a JSON schema on OpenAI-compatible endpoints, structured outputs in vLLM via &lt;code&gt;guided_json&lt;/code&gt;, Outlines, or plain grammar-constrained sampling in llama.cpp.&lt;/p&gt;

&lt;p&gt;The point is that the model can no longer produce an unparseable output. The constraint is applied during sampling, not checked afterwards.&lt;/p&gt;

&lt;p&gt;Re-ran the same 100 questions: &lt;strong&gt;0.70, with a 100% answer rate.&lt;/strong&gt; The categories that had been at zero came back at 0.82 and 0.75.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second-order trap
&lt;/h2&gt;

&lt;p&gt;I hit a related version of this on a larger model in the same family, and the fix was less obvious.&lt;/p&gt;

&lt;p&gt;That model also truncated, on 36 of 100 questions. I had a fallback that recovered an answer letter from the tail of a truncated trace, so those rows produced &lt;em&gt;something&lt;/em&gt;. The score came out 0.67.&lt;/p&gt;

&lt;p&gt;But the recovered answers were junk in a specific, dangerous way. Predicted answers skewed heavily toward option A (48 predictions of A against 23 in the ground truth). When the model gets cut off mid-reasoning, the letter you scrape from the tail is not a decision, it is whatever token happened to be nearby. That is position bias, and it looks exactly like a real answer to your scorer.&lt;/p&gt;

&lt;p&gt;Rows where the reasoning actually completed scored 0.83. I re-ran just the truncated rows with a budget of 8192 instead of 3072, and the merged score came to 0.73.&lt;/p&gt;

&lt;p&gt;So: tail-recovery is worse than a non-answer. A non-answer is visibly missing. A biased recovered answer silently contaminates your accuracy in a direction you did not choose.&lt;/p&gt;

&lt;p&gt;Note also that 16 of those 36 questions &lt;em&gt;still&lt;/em&gt; truncated at 8192 tokens. 0.73 remains a floor, not a measurement.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Match the evaluator's generation budget to the training or intended-use budget.&lt;/strong&gt; Mismatched budgets scored 31% of my rows incorrectly in one run. If you fine-tuned at 512 tokens, do not evaluate at 128.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track truncation as a first-class metric.&lt;/strong&gt; Log &lt;code&gt;clipped_ratio&lt;/code&gt; and &lt;code&gt;parseable_rate&lt;/code&gt; next to accuracy. If either moves, your accuracy number is not comparable to the previous run. I now fail a run outright if the parse-failure count is anything other than what I expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distinguish infrastructure failure from poor performance.&lt;/strong&gt; An unparseable output is a harness event, not a model event. They must be counted separately, always. A weak model still produces well-formed answers and simply scores badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never let a non-answer score as wrong by default.&lt;/strong&gt; Make it raise, or count it in its own bucket. Silently mapping "I could not read this" to "the model was incorrect" is how you get a confident, published, wrong conclusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify the harness against a known-good signal before trusting any number it emits.&lt;/strong&gt; An evaluation harness that has never been checked against a baseline you can sanity-check by hand is untested code that produces numbers, not a measurement instrument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it used to
&lt;/h2&gt;

&lt;p&gt;Reasoning models broke an assumption that free-text answer parsing quietly relied on for years: that a model's answer appears in its output. With extended chain-of-thought, the answer arrives last, and last is exactly what a token budget truncates.&lt;/p&gt;

&lt;p&gt;Every leaderboard comparing a thinking model against a non-thinking one under a shared token budget is, at minimum, measuring something other than what it claims. The thinking model pays for its reasoning out of the same budget that has to carry its answer.&lt;/p&gt;

&lt;p&gt;I published 0.31 as a capability limit and had to retract it. The model was never the problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about ML evaluation, robotics data pipelines, and the ways measurement quietly fails. &lt;a href="https://linkedin.com/in/rickeshnatarajan" rel="noopener noreferrer"&gt;linkedin.com/in/rickeshnatarajan&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>evaluation</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
