<?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: AI Engineering Illustrated</title>
    <description>The latest articles on DEV Community by AI Engineering Illustrated (@aiengillustrated).</description>
    <link>https://dev.to/aiengillustrated</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%2F4179417%2F9cc25513-d413-4604-bf71-3e95f801ee1e.png</url>
      <title>DEV Community: AI Engineering Illustrated</title>
      <link>https://dev.to/aiengillustrated</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aiengillustrated"/>
    <language>en</language>
    <item>
      <title>How Cursor Got Thousands of AI Agents to Build One Browser</title>
      <dc:creator>AI Engineering Illustrated</dc:creator>
      <pubDate>Wed, 14 Oct 2026 10:45:27 +0000</pubDate>
      <link>https://dev.to/aiengillustrated/how-cursor-got-thousands-of-ai-agents-to-build-one-browser-3loh</link>
      <guid>https://dev.to/aiengillustrated/how-cursor-got-thousands-of-ai-agents-to-build-one-browser-3loh</guid>
      <description>&lt;p&gt;&lt;em&gt;Locks failed, one boss agent burned out, and a tree of planners won. Here's what Cursor's multi-agent experiments teach about coordinating agents at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One coding agent is good at a focused task: fix this bug, add this endpoint, write these tests. Give it something the size of a web browser and it falls apart. Cursor engineer Wilson Lin tried exactly that. He asked a frontier model to plan a browser engine and kept telling it to "keep going." The model &lt;a href="https://cursor.com/blog/self-driving-codebases" rel="noopener noreferrer"&gt;lost track of what it was doing&lt;/a&gt;, declared success long before it was finished, and got stuck on hard details.&lt;/p&gt;

&lt;p&gt;So Cursor asked a different question: if you spend 10× more compute, can you get 10× more useful work? In a research project that ran for a few weeks around the start of 2026, the team built a harness that ran &lt;strong&gt;hundreds of agents per machine, about 2,000 at peak&lt;/strong&gt;, on a single codebase. It wrote a browser engine called FastRender with more than a million lines of Rust. At its peak, the system landed &lt;strong&gt;about 1,000 commits an hour&lt;/strong&gt; with no human steering.&lt;/p&gt;

&lt;p&gt;The browser was only the test case. The real output was a set of coordination lessons that became a product: &lt;a href="https://cursor.com/blog/projects" rel="noopener noreferrer"&gt;Cursor Projects&lt;/a&gt;, launched September 10, 2026, where one coordinator agent directs thousands of subagents. This issue walks through the four designs Cursor tried, why three failed, how the winner works, and what it cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: Why One Agent Isn't Enough
&lt;/h2&gt;

&lt;p&gt;A few terms first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; a language model calling tools in a loop: read files, run commands, edit code, repeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness:&lt;/strong&gt; the program around the model that runs that loop, supplies tools and manages context. (Issue 005 took a single-agent harness apart.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent system:&lt;/strong&gt; several agents, each with its own context window, splitting up one goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput:&lt;/strong&gt; useful work per hour. For Cursor, mostly commits that move the project forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single agent has two hard limits on a big project. It does &lt;strong&gt;one thing at a time&lt;/strong&gt;, and it has &lt;strong&gt;one context window&lt;/strong&gt; that fills with old tool output until the agent loses the plot. Many agents, each with a fresh context, fix both in principle. In practice they create the problem every human team has: &lt;strong&gt;coordination&lt;/strong&gt;. Who works on what? How do you stop two agents fixing the same bug? Who decides the project is done?&lt;/p&gt;

&lt;p&gt;Cursor's research is useful because it reports the designs that &lt;em&gt;didn't&lt;/em&gt; work. Lin describes it in two posts: &lt;a href="https://cursor.com/blog/scaling-agents" rel="noopener noreferrer"&gt;Scaling long-running autonomous coding&lt;/a&gt; (January 14, 2026) and the longer &lt;a href="https://cursor.com/blog/self-driving-codebases" rel="noopener noreferrer"&gt;Towards self-driving codebases&lt;/a&gt; (February 5, 2026). Everything below comes from those posts unless another source is linked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Harness Designs, Three Failures
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmszylpkrgai9qy33ara.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmszylpkrgai9qy33ara.png" alt="Cursor's four harness designs: flat peers with locks, planner-executor-judge, a continuous executor, and the final planner tree" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The team started with a simple Rust harness on &lt;strong&gt;one large Linux VM&lt;/strong&gt;, controlled over SSH. Before tuning anything, they logged every agent message, system action and command output with timestamps, so sessions could be replayed and mined for patterns. That observability made every later step possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Flat peers and a lock file.&lt;/strong&gt; The first idea was the least prescriptive: every agent has the same role, and they coordinate through a shared state file. An agent reads the file to see what others are doing, claims a task and updates its status, with a lock so two agents can't grab the same task.&lt;/p&gt;

&lt;p&gt;It failed fast. Agents held locks too long, forgot to release them, or edited the file without locking at all. Even when locking worked, &lt;strong&gt;20 agents slowed to the throughput of 2 or 3&lt;/strong&gt;, mostly waiting. Swapping locks for &lt;strong&gt;optimistic concurrency control&lt;/strong&gt; (read freely; a write fails if the state changed since you read it) was more robust, but exposed a deeper problem. With no hierarchy, agents became &lt;strong&gt;risk-averse&lt;/strong&gt;, picking small, safe changes. No one owned the project, so work churned without progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Planner, executor, workers and a judge.&lt;/strong&gt; Next, roles. A &lt;strong&gt;planner&lt;/strong&gt; wrote the approach and deliverables. An &lt;strong&gt;executor&lt;/strong&gt; owned delivering that plan and spawned &lt;strong&gt;workers&lt;/strong&gt; to do the tasks. After each round, an independent &lt;strong&gt;judge&lt;/strong&gt; decided whether the work was finished or another iteration should run. This fixed most coordination problems, because a single agent now owned the outcome. But it was rigid. Each round waited for the &lt;strong&gt;slowest worker&lt;/strong&gt;, and because all planning happened up front, the system couldn't adjust when it discovered new problems mid-round.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The continuous executor.&lt;/strong&gt; So Cursor removed the separate planner. The executor planned and spawned tasks continuously, and to keep it from drifting over long runs the team added "freshness" mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;scratchpad.md&lt;/code&gt; that agents &lt;strong&gt;rewrite&lt;/strong&gt; rather than append to,&lt;/li&gt;
&lt;li&gt;automatic summarizing when an agent nears its context limit,&lt;/li&gt;
&lt;li&gt;self-reflection and alignment reminders in the system prompts,&lt;/li&gt;
&lt;li&gt;explicit permission to pivot and challenge assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked well enough that the judge was dropped. Then the executor started misbehaving: sleeping at random, doing work itself, spawning only a few narrow tasks, merging badly and claiming early completion. The diagnosis: too many jobs at once (plan, explore, spawn, monitor, review, edit, merge, decide when to stop). One agent was doing a whole management chain's work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A tree of planners, workers and handoffs.&lt;/strong&gt; The final design split those jobs back apart, but this time into a hierarchy that can grow. That's the design the rest of this issue is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Final System Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdthuxpw069ksy97rbksz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdthuxpw069ksy97rbksz.png" alt="The final design: a root planner, recursive subplanners, isolated workers, and handoffs flowing back up" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Following the numbers in the diagram:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You write the instructions.&lt;/strong&gt; A human gives the system its goal and constraints. Once a run starts, Lin told &lt;a href="https://simonwillison.net/2026/Jan/23/fastrender/" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt;, "there's actually no way to steer it." The only control is stopping it, so the instructions carry a lot of weight (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The root planner owns the whole scope.&lt;/strong&gt; It studies the current state of the repo and writes specific, targeted tasks that move toward the goal. It &lt;strong&gt;writes no code itself&lt;/strong&gt;, and it doesn't know or care which agent picks up each task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planners spawn subplanners.&lt;/strong&gt; When a planner decides its scope can be split, it creates subplanners that each fully own one slice, say the CSS engine. This is &lt;strong&gt;recursive&lt;/strong&gt;, so planning itself runs in parallel, and no single planner gets tunnel vision on a huge project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers take one task each.&lt;/strong&gt; A worker drives its task to completion and knows nothing about the larger system. It doesn't talk to other workers or planners, and it works on &lt;strong&gt;its own copy of the repo&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers push to one shared branch.&lt;/strong&gt; There's no central merge gate. Workers resolve their own conflicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A handoff goes back up.&lt;/strong&gt; When a worker finishes, it writes a single &lt;strong&gt;handoff&lt;/strong&gt; that the system delivers to the planner that asked for the task. The handoff covers what was done plus notes, concerns, deviations, findings and feedback. The planner gets it as a follow-up message, pulls the latest code and plans again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 6 is the subtle part. Even a planner that thinks it's "done" keeps receiving handoffs, so it keeps planning. Information flows &lt;strong&gt;up&lt;/strong&gt; the tree toward agents with an increasingly global view, without any global synchronization or cross-talk between workers. Lin notes that this looks a lot like how some human software teams work, even though the models were never trained to organize this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why there's no integrator.&lt;/strong&gt; Cursor originally added an &lt;strong&gt;integrator&lt;/strong&gt; agent for quality control and to serialize the pushes, rebases and merges from hundreds of workers. It became the obvious bottleneck: hundreds of workers and one gate. Prompt changes didn't fix it, so the team removed it. Workers turned out to be capable of handling conflicts themselves. And conflicts were rarer than you might expect. Lin told Willison that &lt;strong&gt;most commits had no merge conflicts&lt;/strong&gt;, because planners split the work so that tasks rarely overlapped in time or files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different models for different roles.&lt;/strong&gt; Cursor found OpenAI's GPT-5.2 better at extended autonomous work than Claude Opus 4.5, which "tends to stop earlier and take shortcuts." GPT-5.2 was also a better &lt;strong&gt;planner&lt;/strong&gt; than the coding-specialized GPT-5.1-Codex, because the job was "more expansive than merely coding," Lin said: operating inside a harness without a user to ask. The system now picks a model per role.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throughput Over Perfection
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw0c6qiwixeytgitbjp3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw0c6qiwixeytgitbjp3.png" alt="Requiring every commit to be green serialized the system; accepting a small, stable error rate let it scale" width="800" height="973"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most counterintuitive decision in the project is about correctness. At first, Cursor required &lt;strong&gt;100% correctness before every commit&lt;/strong&gt;. That caused heavy serialization. One small error, like an API change or a typo, could halt the whole system. Workers wandered outside their scope to fix unrelated breakage, and many agents piled onto the same issue and trampled each other.&lt;/p&gt;

&lt;p&gt;So the team allowed some slack. If a commit introduces a small error, agents trust that another agent will fix it soon, which is true because every part of the codebase has an owner in the tree. The result, in Lin's words, is an error rate that "remains small and constant," not one that grows. He suggests the ideal setup &lt;strong&gt;accepts some error rate&lt;/strong&gt; and adds a final &lt;strong&gt;"green" branch&lt;/strong&gt;, where an agent regularly takes a snapshot and does a quick fix-up pass before release.&lt;/p&gt;

&lt;p&gt;The same logic applies when two agents touch the same file. Rather than over-engineer a prevention scheme, the harness accepts "moments of turbulence" and lets the code settle. That costs some tokens but keeps the system simple enough to align and observe.&lt;/p&gt;

&lt;p&gt;The numbers from the stable run: &lt;strong&gt;about 1,000 commits per hour across 10 million tool calls over one week&lt;/strong&gt;, with no human intervention after launch. By the time Willison wrote about it, the repo had nearly 30,000 commits.&lt;/p&gt;

&lt;p&gt;This is a classic distributed-systems trade-off, made consciously: &lt;strong&gt;give up per-commit consistency to get throughput, then reconcile at the end&lt;/strong&gt;. It's similar to eventual consistency in a database, applied to a codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Machine Became the Bottleneck
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frlncu5stnrdcvxzqfiot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frlncu5stnrdcvxzqfiot.png" alt="One big VM per run: once agent RAM was capped, disk I/O and shared tool locks became the limits" width="800" height="987"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cursor deliberately avoided building a distributed system. &lt;strong&gt;Each multi-agent run had its own large machine&lt;/strong&gt;, which made metrics easy to watch and state easy to share. Lin told Willison each machine ran about &lt;strong&gt;300 agents&lt;/strong&gt;, which works because agents spend much of their time thinking rather than running tools. To go beyond one machine, the team split the browser into separate work streams (for example, CSS) and ran one harness per stream on its own machine.&lt;/p&gt;

&lt;p&gt;Scaling up exposed bottlenecks that a single developer never sees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAM first.&lt;/strong&gt; After agent memory was capped, the hotspot moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then disk.&lt;/strong&gt; With a monolithic project, hundreds of agents compiling at once read and wrote &lt;strong&gt;many GB/s of build artifacts&lt;/strong&gt;. Compilation, not thinking, came to dominate time. A later run restructured the project into many self-contained crates and spent far less time waiting on builds, running at "multiple times more throughput."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then shared locks in tools.&lt;/strong&gt; Git and Cargo use shared locks as simple concurrency control. The team noticed contention from many agents running &lt;code&gt;git restore&lt;/code&gt; at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lin's takeaway: &lt;strong&gt;codebase structure affects token throughput&lt;/strong&gt;. He floats borrowing from databases and storage systems: finer concurrency control instead of coarse tool locks, and copy-on-write plus deduplication for hundreds of near-identical repo copies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback loops kept agents honest.&lt;/strong&gt; A week-long autonomous run needs signals that tell agents whether they're right. FastRender used three, according to Lin's interview with Willison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specs in the repo.&lt;/strong&gt; Git submodules pulled in the CSS, HTML, DOM and ECMAScript specifications, and the agent-written code often cites them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots vs. golden images.&lt;/strong&gt; GPT-5.2 can read images, so agents compared rendered screenshots against golden samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Rust compiler.&lt;/strong&gt; "You can get a lot of verification just from compilation."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Instructions Get Amplified
&lt;/h2&gt;

&lt;p&gt;The harness turned out to be the easy part. The &lt;strong&gt;instructions&lt;/strong&gt; were harder. A multi-agent run is like a normal coding agent with "orders of magnitude more time and compute," Lin writes, and that "amplifies everything, including suboptimal and unclear instructions." Agents follow instructions to the end, good or bad.&lt;/p&gt;

&lt;p&gt;Examples from the browser project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Spec implementation" was vague enough that agents dove into obscure, rarely used features instead of prioritizing.&lt;/li&gt;
&lt;li&gt;Performance expectations were assumed, not stated. It took explicit instructions and enforced timeouts to get agents to balance speed with other goals.&lt;/li&gt;
&lt;li&gt;With no stated dependency policy, agents pulled in libraries they could have written. One added QuickJS to unblock itself while other agents were still building the JavaScript engine. A later run listed the banned libraries, which fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From all this, Cursor drew a few prompting rules that apply well beyond browsers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only instruct what the model doesn't know&lt;/strong&gt;, like multi-agent etiquette or how to run your tests. Treat it like "a brilliant new hire" who knows engineering but not your codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints beat instructions.&lt;/strong&gt; "No TODOs, no partial implementations" works better than "remember to finish implementations."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid checklists for deep work.&lt;/strong&gt; A list of specific items makes the model focus on the list and implicitly deprioritize everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use numbers for scope.&lt;/strong&gt; "Generate many tasks" gets a cautious handful. "Generate 20–100 tasks" signals ambition, and the team saw very different behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cursor's January post sums it up: "The harness and models matter, but the prompts matter more."&lt;/p&gt;

&lt;h2&gt;
  
  
  How Other Teams Coordinate Agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxh4kqkc3uuekf1kdc5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxh4kqkc3uuekf1kdc5i.png" alt="Five multi-agent systems compared: how they coordinate, their scale and how they check work" width="800" height="1093"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cursor's tree isn't the only working design. Comparing it with two Anthropic write-ups shows how much the right structure depends on the task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic's C compiler: peers with git locks.&lt;/strong&gt; Nicholas Carlini ran &lt;a href="https://www.anthropic.com/engineering/building-c-compiler" rel="noopener noreferrer"&gt;16 Claude agents&lt;/a&gt; in parallel, each in its own Docker container with its own clone of a shared repo. There's no orchestrator. An agent claims a task by writing a file to &lt;code&gt;current_tasks/&lt;/code&gt;, and git's synchronization forces a second agent that claims the same task to pick another one. Over nearly 2,000 Claude Code sessions, two billion input tokens and just under $20,000, they produced a 100,000-line Rust C compiler that builds Linux 6.9 on x86, ARM and RISC-V.&lt;/p&gt;

&lt;p&gt;Two of Carlini's lessons mirror Cursor's. First, &lt;strong&gt;the verifier decides everything&lt;/strong&gt;: if it isn't "nearly perfect," Claude "will solve the wrong problem." Second, flat parallelism breaks on one giant task. When all 16 agents hit the same Linux-kernel bug, they overwrote each other's fixes. The fix was GCC as a known-good &lt;strong&gt;oracle&lt;/strong&gt;: compile most kernel files with GCC and a random subset with Claude's compiler, so each agent could chase a different failing file. The test suite, not a planner, splits the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic's Research feature: one lead, parallel subagents.&lt;/strong&gt; In &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;Claude's Research system&lt;/a&gt;, a lead agent plans, spawns 3–5 subagents that search in parallel, and synthesizes their findings. An Opus 4 lead with Sonnet 4 subagents beat single-agent Opus 4 by &lt;strong&gt;90.2%&lt;/strong&gt; on Anthropic's internal research eval, at about &lt;strong&gt;15× the tokens of a chat&lt;/strong&gt;. Token usage alone explained 80% of performance variance on BrowseComp. The lead waits for each batch of subagents to finish, which Anthropic calls a bottleneck: the same "slowest worker" problem as Cursor's design 2. Anthropic also notes that most coding tasks have fewer truly parallel pieces than research.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor's own follow-ups.&lt;/strong&gt; The same harness later took on &lt;a href="https://cursor.com/blog/multi-agent-kernels" rel="noopener noreferrer"&gt;235 CUDA kernel problems with NVIDIA&lt;/a&gt;, with a planner rebalancing work by benchmark results. Over three weeks it reached a &lt;strong&gt;38% geometric-mean speedup&lt;/strong&gt; over baselines, and the benchmark threw out any "cheated" result faster than a B200 can physically run. After a February &lt;a href="https://cursor.com/blog/long-running-agents" rel="noopener noreferrer"&gt;research preview of long-running agents&lt;/a&gt; (plan first, then have agents check each other's work), September brought &lt;strong&gt;Projects&lt;/strong&gt;: a coordinator that "doesn't write code itself but directs other agents that do," with shared context files synced to every agent and subscriptions to Slack, schedules and PRs. Cursor says users who mainly work through Projects merge six times as many PRs.&lt;/p&gt;

&lt;p&gt;The common thread across all three teams: &lt;strong&gt;separate the agents that decide what to do from the agents that do it&lt;/strong&gt;, give every worker a fresh context and an isolated workspace, and invest heavily in automated checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-offs (and the Skeptics)
&lt;/h2&gt;

&lt;p&gt;The approach has real costs, and the critics have a point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code quality.&lt;/strong&gt; Software Improvement Group &lt;a href="https://www.softwareimprovementgroup.com/blog/quality-of-fastrender/" rel="noopener noreferrer"&gt;ran the public FastRender code&lt;/a&gt; through its analysis platform. It scored &lt;strong&gt;1.3 out of 5 for maintainability&lt;/strong&gt;, in the bottom 5% of systems SIG sees, and 2.1 out of 5 for architecture, meaning tightly coupled components. SIG sized the code at roughly 110 person-years of Rust and warned of "a new class of legacy: large, opaque, and hard to trust." Cursor never presented FastRender as production software, and the browser's own quirks (JavaScript turned off behind a feature flag, vendored layout libraries) show it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"From scratch" is debatable.&lt;/strong&gt; The agents pulled in dependencies such as Taffy for flexbox and grid layout. Critics quoted by SIG note that the agents' planning documents used Servo and Chromium as guides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; "Trillions of tokens," Cursor writes. SIG cites estimates of 10 to 20 trillion, several million dollars at list prices. Even Anthropic's far smaller compiler run cost about $20,000. The work has to be worth the tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification is the ceiling.&lt;/strong&gt; Carlini's compiler started breaking existing features with each new one near the end of the project. Cursor's final "green branch" pass is still a proposal. Carlini, who used to work in penetration testing, worries about "programmers deploying software they've never personally verified."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mid-run steering.&lt;/strong&gt; The research harness could only be stopped, not redirected. Projects adds a coordinator you can chat with mid-run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure beats self-organization.&lt;/strong&gt; Equal agents sharing a lock file were slow (20 agents ≈ 2–3) and risk-averse. Clear ownership through planners, workers and handoffs is what scaled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate deciding from doing, and keep each role small.&lt;/strong&gt; One agent juggling planning, merging and judging broke down. Recursive subplanners and narrow workers didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput needs slack.&lt;/strong&gt; Accepting a small, stable error rate plus a final fix-up pass beat requiring every commit to be green. Removing the central integrator also helped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The environment is part of the system.&lt;/strong&gt; Disk I/O, tool locks, build structure and feedback signals (specs, screenshots, compilers, test oracles) shaped results as much as the models did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instructions get amplified.&lt;/strong&gt; At thousands of agents, vague specs become wasted weeks. Use constraints, explicit numbers and a stated policy for dependencies and performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading (sources used)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Wilson Lin (Cursor), &lt;a href="https://cursor.com/blog/scaling-agents" rel="noopener noreferrer"&gt;Scaling long-running autonomous coding&lt;/a&gt; (Jan 14, 2026)&lt;/li&gt;
&lt;li&gt;Wilson Lin (Cursor), &lt;a href="https://cursor.com/blog/self-driving-codebases" rel="noopener noreferrer"&gt;Towards self-driving codebases&lt;/a&gt; (Feb 5, 2026), the detailed harness write-up&lt;/li&gt;
&lt;li&gt;Simon Willison, &lt;a href="https://simonwillison.net/2026/Jan/23/fastrender/" rel="noopener noreferrer"&gt;Wilson Lin on FastRender: a browser built by thousands of parallel agents&lt;/a&gt; (interview notes: ~2,000 concurrent agents, ~300 per machine, feedback loops)&lt;/li&gt;
&lt;li&gt;FastRender source: &lt;a href="https://github.com/wilsonzlin/fastrender" rel="noopener noreferrer"&gt;github.com/wilsonzlin/fastrender&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cursor, &lt;a href="https://cursor.com/blog/long-running-agents" rel="noopener noreferrer"&gt;Expanding our long-running agents research preview&lt;/a&gt; (Feb 12, 2026) and &lt;a href="https://cursor.com/blog/projects" rel="noopener noreferrer"&gt;Introducing Projects&lt;/a&gt; (Sep 10, 2026)&lt;/li&gt;
&lt;li&gt;Cursor and NVIDIA, &lt;a href="https://cursor.com/blog/multi-agent-kernels" rel="noopener noreferrer"&gt;Speeding up GPU kernels by 38% with a multi-agent system&lt;/a&gt; (Apr 14, 2026)&lt;/li&gt;
&lt;li&gt;Nicholas Carlini (Anthropic), &lt;a href="https://www.anthropic.com/engineering/building-c-compiler" rel="noopener noreferrer"&gt;Building a C compiler with a team of parallel Claudes&lt;/a&gt; (Feb 5, 2026)&lt;/li&gt;
&lt;li&gt;Anthropic, &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;How we built our multi-agent research system&lt;/a&gt; (Jun 13, 2025)&lt;/li&gt;
&lt;li&gt;Software Improvement Group, &lt;a href="https://www.softwareimprovementgroup.com/blog/quality-of-fastrender/" rel="noopener noreferrer"&gt;We analyzed the code of Cursor's AI-built browser FastRender&lt;/a&gt; (Feb 19, 2026)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Try it in miniature: give one agent a planner prompt that only writes task files, run three worker agents that each take one task in their own git worktree and write a short handoff when done, and watch what the planner does with the handoffs.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ragmehta.substack.com/p/how-cursor-got-thousands-of-ai-agents" rel="noopener noreferrer"&gt;AI Engineering Illustrated&lt;/a&gt;. Subscribe for new deep dives.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>agents</category>
      <category>cursor</category>
    </item>
    <item>
      <title>Why Can't LLMs Count the R's in "Strawberry"? How Tokenizers Work</title>
      <dc:creator>AI Engineering Illustrated</dc:creator>
      <pubDate>Tue, 13 Oct 2026 22:49:40 +0000</pubDate>
      <link>https://dev.to/aiengillustrated/why-cant-llms-count-the-rs-in-strawberry-how-tokenizers-work-eib</link>
      <guid>https://dev.to/aiengillustrated/why-cant-llms-count-the-rs-in-strawberry-how-tokenizers-work-eib</guid>
      <description>&lt;p&gt;&lt;em&gt;The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask a language model how many r's are in "strawberry" and, for a long time, the popular answer was "two." The same models could write a sonnet about strawberries, explain their genetics and translate the word into forty languages. So why does counting letters go wrong?&lt;/p&gt;

&lt;p&gt;The model never sees the letters. Before your prompt reaches the first transformer layer, a separate program called the &lt;strong&gt;tokenizer&lt;/strong&gt; chops the text into chunks and swaps each chunk for an integer. The model only ever reads those integers. If "strawberry" becomes a single integer, the three r's are hidden inside a number the model can't take apart.&lt;/p&gt;

&lt;p&gt;Tokenization is the least glamorous part of the LLM stack, and it causes a surprising number of problems: shaky multi-digit arithmetic, higher costs for non-English users, strange "glitch" words that make models act up, and token counts that don't match your character-based estimates. In this issue we'll open the tokenizer up: how it turns text into IDs, how Byte Pair Encoding (BPE) learns its vocabulary, and how its choices show up as spelling, arithmetic, language and glitch-token problems you can design around.&lt;/p&gt;

&lt;p&gt;Every token split in this issue was measured with OpenAI's open-source &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; library on the actual GPT-2, GPT-4 (&lt;code&gt;cl100k_base&lt;/code&gt;) and GPT-4o (&lt;code&gt;o200k_base&lt;/code&gt;) vocabularies, so you can reproduce them yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens: the Model's Only Alphabet
&lt;/h2&gt;

&lt;p&gt;A neural network can't do math on strings. It needs numbers. The simplest scheme would give every &lt;em&gt;character&lt;/em&gt; a number, but then a 2,000-word document becomes more than 10,000 positions, and every transformer layer pays for each one. The other extreme gives every &lt;em&gt;word&lt;/em&gt; a number, but no fixed list covers every word, name, typo, URL and code identifier people will ever type.&lt;/p&gt;

&lt;p&gt;Rico Sennrich and colleagues measured this trade-off in their 2016 paper &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Neural Machine Translation of Rare Words with Subword Units&lt;/a&gt;. On a German training corpus of about 100 million words, keeping whole words needed &lt;strong&gt;1.75 million&lt;/strong&gt; vocabulary entries and still left 1,079 unknown words in the test set. Splitting into characters needed only about 3,000 symbols but stretched the corpus to &lt;strong&gt;550 million&lt;/strong&gt; positions. Their subword method, BPE, landed in between: about 63,000 symbols, 112 million positions and &lt;strong&gt;zero&lt;/strong&gt; unknown words.&lt;/p&gt;

&lt;p&gt;That middle ground is a &lt;strong&gt;token&lt;/strong&gt;: a chunk of text that's usually shorter than a word for rare words and exactly one word for common ones. Modern LLMs use vocabularies of 50,000 to 200,000 tokens. tiktoken's README puts the average at about &lt;strong&gt;4 bytes of text per token&lt;/strong&gt;. On the English text of last week's issue I measured 4.4 characters per token with the GPT-4o tokenizer, or roughly 0.63 words per token.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Text to Token IDs in Five Steps
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F57n216lwbaawee3imfwq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F57n216lwbaawee3imfwq.png" alt="From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed" width="800" height="1053"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens to the sentence "I love strawberry" on its way into GPT-4o:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raw text.&lt;/strong&gt; Your string arrives as UTF-8 bytes. Some tokenizers normalize it first (Unicode normalization, lowercasing in older models), but GPT-style tokenizers keep the bytes exactly as typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-split.&lt;/strong&gt; A regular expression cuts the text into pieces that merges are never allowed to cross: letter runs, digit runs, punctuation, whitespace. The &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;tiktoken source&lt;/a&gt; shows the patterns. GPT-2's version keeps a leading space attached to the following word, so the pieces are &lt;code&gt;I&lt;/code&gt;, &lt;code&gt;love&lt;/code&gt;, &lt;code&gt;strawberry&lt;/code&gt;. That's why the dot in the figure matters: &lt;strong&gt;" strawberry" with a space and "strawberry" without one are different tokens.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BPE merges.&lt;/strong&gt; Each piece starts as a sequence of bytes. The tokenizer then applies its learned merge rules, in the order they were learned, gluing adjacent pairs together until no rule applies. Common words collapse into one token. Rare ones end up as a few subword chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token IDs.&lt;/strong&gt; Each final chunk is looked up in the vocabulary. With &lt;code&gt;o200k_base&lt;/code&gt;, "I love strawberry" becomes &lt;code&gt;[40, 3047, 101830]&lt;/code&gt;: three integers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings.&lt;/strong&gt; Inside the model, each ID selects one row of a learned &lt;strong&gt;embedding matrix&lt;/strong&gt;, a vector of a few thousand numbers. From here on, the transformer only ever works with those vectors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two properties fall out of this design, and the tiktoken README calls both out. Tokenization is &lt;strong&gt;lossless&lt;/strong&gt;: decoding the IDs gives back the exact original bytes. And it &lt;strong&gt;works on any text&lt;/strong&gt;, even strings that never appeared when the tokenizer was trained. The second property depends on a trick we'll get to in a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BPE Learns Its Vocabulary
&lt;/h2&gt;

&lt;p&gt;Byte Pair Encoding started life as a data compression algorithm (Philip Gage, 1994): repeatedly find the most common pair of adjacent bytes and replace it with a new, unused symbol. Sennrich et al. adapted the idea to build a vocabulary instead of compressing a file. Training goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split every word in the training corpus into characters. The starting vocabulary is just those characters.&lt;/li&gt;
&lt;li&gt;Count every pair of adjacent symbols, weighted by how often each word appears.&lt;/li&gt;
&lt;li&gt;Merge the most frequent pair into a new symbol, add it to the vocabulary, and record the rule.&lt;/li&gt;
&lt;li&gt;Repeat until the vocabulary reaches the target size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The number of merges is the only real setting. As the paper puts it, the final vocabulary size equals the starting symbols plus the number of merge operations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1smiat1h5yy3j5xd8ee6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1smiat1h5yy3j5xd8ee6.png" alt="BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full" width="800" height="1067"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagram uses the toy corpus from the &lt;a href="https://huggingface.co/learn/llm-course/chapter6/5" rel="noopener noreferrer"&gt;Hugging Face LLM course&lt;/a&gt;: "hug" (10 times), "pug" (5), "pun" (12), "bun" (4) and "hugs" (5).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start:&lt;/strong&gt; the vocabulary is &lt;code&gt;b g h n p s u&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 1:&lt;/strong&gt; the pair &lt;code&gt;u + g&lt;/code&gt; appears 20 times (10 in hug, 5 in pug, 5 in hugs), more than any other, so &lt;code&gt;ug&lt;/code&gt; becomes a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 2:&lt;/strong&gt; &lt;code&gt;u + n&lt;/code&gt; now leads with 16 (12 in pun, 4 in bun), so &lt;code&gt;un&lt;/code&gt; joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 3:&lt;/strong&gt; &lt;code&gt;h + ug&lt;/code&gt; appears 15 times, and the first three-letter token, &lt;code&gt;hug&lt;/code&gt;, is born.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real tokenizers run this loop tens of thousands of times over gigabytes of text. GPT-2's vocabulary has 50,257 entries. GPT-4's &lt;code&gt;cl100k_base&lt;/code&gt; has about 100,000 and GPT-4o's &lt;code&gt;o200k_base&lt;/code&gt; about 200,000 (100,277 and 200,019 including special tokens, according to tiktoken). Meta says Llama 3 moved to a &lt;a href="https://ai.meta.com/blog/meta-llama-3/" rel="noopener noreferrer"&gt;128K-token vocabulary&lt;/a&gt; that yields up to 15% fewer tokens than Llama 2 on the same text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoding new text&lt;/strong&gt; replays the merges. Split the input into characters (or bytes), then apply the learned rules in priority order. A word seen often during training collapses into one token. A word never seen still gets encoded, just in smaller pieces. With GPT-4o's vocabulary, "unbelievably" becomes &lt;code&gt;un | bel | ievably&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The order of merges also explains some quirks. Merges reflect the &lt;em&gt;tokenizer's&lt;/em&gt; training data, which isn't necessarily the &lt;em&gt;model's&lt;/em&gt; training data. Keep that in mind; it comes back when we get to glitch tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Byte-Level BPE: No More "Unknown Word"
&lt;/h2&gt;

&lt;p&gt;Classic BPE starts from characters, and Unicode defines roughly 150,000 of them. Any character missing from the training corpus becomes &lt;code&gt;[UNK]&lt;/code&gt;, the "unknown" token. The Hugging Face course notes this is one reason older NLP models handled emoji badly.&lt;/p&gt;

&lt;p&gt;GPT-2 introduced a simple fix: run BPE over &lt;strong&gt;bytes&lt;/strong&gt; instead of characters. There are only 256 possible byte values, so the base vocabulary has 256 entries and every possible input, whether emoji, Tamil script or binary garbage, can be written in them. Merges then build up common multi-byte sequences. As the course puts it, GPT-2 doesn't actually have an unknown token, because an unknown character can't occur with byte-level BPE.&lt;/p&gt;

&lt;p&gt;You can see the byte fallback at work. In GPT-2's vocabulary, the emoji 🙂 (four UTF-8 bytes) becomes 2 tokens. GPT-4o's larger vocabulary learned the whole sequence, so it's 1 token. Either way, nothing is ever "unknown." The price is that a rarely seen script can get chopped into fragments that aren't even whole characters, which leads to the language tax below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Model Can't See the R's
&lt;/h2&gt;

&lt;p&gt;Back to the strawberry. Here's what GPT-4o's tokenizer does with the classic question:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbpq9njyu2m4kurweml8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbpq9njyu2m4kurweml8.png" alt="The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters" width="800" height="1000"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"How many r's are in strawberry?" becomes 8 tokens: &lt;code&gt;How | ·many | ·r | 's | ·are | ·in | ·strawberry | ?&lt;/code&gt;. The word you care about is token &lt;strong&gt;#101830&lt;/strong&gt;. To the model, that's one opaque symbol, mapped to one embedding vector. There's no "r" in it to count.&lt;/p&gt;

&lt;p&gt;Whatever the model knows about how strawberry is spelled, it had to &lt;em&gt;memorize&lt;/em&gt; from training text where the word happened to be spelled out, as in dictionaries, spelling-bee pages or kids' worksheets. That knowledge is thin and indirect, so the model guesses, and a plausible guess for a word ending in a double letter is "two."&lt;/p&gt;

&lt;p&gt;The split also depends on context in ways people don't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strawberry&lt;/code&gt; (leading space, the usual case mid-sentence) is &lt;strong&gt;1&lt;/strong&gt; token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strawberry&lt;/code&gt; (no leading space, for example at the start of a message) is &lt;strong&gt;3&lt;/strong&gt; tokens: &lt;code&gt;st | raw | berry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Strawberry&lt;/code&gt; (capitalized) is a different 3: &lt;code&gt;Str | aw | berry&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the model may have learned slightly different things about the same word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fixes all expose the letters.&lt;/strong&gt; Writing the word as &lt;code&gt;s-t-r-a-w-b-e-r-r-y&lt;/code&gt; turns it into 10 tokens, one per letter, and now counting is something the model can actually do. Better still, when exact character-level answers matter (counting, reversing a string, checking a regex), have the model &lt;strong&gt;call a code tool&lt;/strong&gt;. One line of Python, &lt;code&gt;"strawberry".count("r")&lt;/code&gt;, returns 3 every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multilingual Token Tax
&lt;/h2&gt;

&lt;p&gt;The tokenizer's merges come from its training data, and that data is mostly English and code. English words get dedicated tokens. Other languages, especially ones with their own scripts, get fewer merges and are left closer to raw bytes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqemiok76vshav7x8gk9y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqemiok76vshav7x8gk9y.png" alt="The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks" width="800" height="1093"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I ran one short sentence, "The weather is nice today," and simple translations of it through three of OpenAI's tokenizers. In English, it's 6 tokens in all three. In Tamil, it's &lt;strong&gt;85 tokens with GPT-2's tokenizer, 42 with GPT-4's and 11 with GPT-4o's&lt;/strong&gt;. Hindi drops from 27 to 21 to 5, and Russian from 27 to 13 to 6. That's just one sentence, not a benchmark, but the direction is clear. Larger, more multilingual vocabularies have closed much of the gap, though not all of it.&lt;/p&gt;

&lt;p&gt;Aleksandar Petrov and colleagues studied this systematically in &lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Language Model Tokenizers Introduce Unfairness Between Languages&lt;/a&gt; (2023). They found the same text can differ in token length by up to &lt;strong&gt;15×&lt;/strong&gt; across languages, and that even byte-level and character-level models show gaps of more than 4×. The consequences are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; APIs bill per token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context:&lt;/strong&gt; a 128K-token window holds far fewer pages of Tamil than of English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; output is generated one token at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you serve a non-English market, measure token counts on your actual traffic. Don't assume the "about 4 characters per token" rule of thumb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers: Why Long Addition Goes Wrong
&lt;/h2&gt;

&lt;p&gt;The same pre-split regex decides how numbers are chunked, and labs have made very different choices. Aaditya Singh and DJ Strouse surveyed them in &lt;a href="https://arxiv.org/abs/2402.14903" rel="noopener noreferrer"&gt;Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs&lt;/a&gt; (2024):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure BPE&lt;/strong&gt; (GPT-3, Claude 2.1, OLMo): whichever digit strings happened to be frequent got tokens. 710 might be one token while 711 isn't, so numbers of the same length split inconsistently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single digits&lt;/strong&gt; (PaLM, Llama 1 and 2, Mistral): every digit is its own token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunks of up to 3 digits, left to right&lt;/strong&gt; (GPT-3.5, GPT-4): the &lt;code&gt;\p{N}{1,3}&lt;/code&gt; rule in the &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;tiktoken pattern&lt;/a&gt;. GPT-4o's &lt;code&gt;o200k_base&lt;/code&gt; uses the same rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Left-to-right chunking means &lt;code&gt;1234567&lt;/code&gt; becomes &lt;code&gt;123 | 456 | 7&lt;/code&gt;. The chunks line up with the &lt;em&gt;start&lt;/em&gt; of the number, while place value, and the way carries move when you add, runs from the &lt;em&gt;end&lt;/em&gt;. Insert commas and the tokenizer splits on them, giving &lt;code&gt;1 | , | 234 | , | 567&lt;/code&gt;. Now every chunk sits on a thousands boundary.&lt;/p&gt;

&lt;p&gt;Singh and Strouse tested exactly this on GPT-3.5 with 7- to 9-digit addition. With plain digits, 8-shot accuracy was &lt;strong&gt;75.6%&lt;/strong&gt;. With commas, it was &lt;strong&gt;97.8%&lt;/strong&gt;. When the answer had more digits than the inputs (because of a final carry), plain-digit accuracy fell to &lt;strong&gt;8.25%&lt;/strong&gt;, and the errors weren't random: the model almost always got the first three digits right and the fourth digit wrong, right at the first token boundary. Asking the model to first rewrite the problem with commas recovered most of the lost accuracy. GPT-4 was less affected, but the effect didn't disappear.&lt;/p&gt;

&lt;p&gt;The lesson isn't "add commas everywhere." It's that &lt;strong&gt;number formatting changes the problem the model has to solve&lt;/strong&gt;. For anything that must be exact, like invoices or financial math, route the arithmetic to a calculator or code tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glitch Tokens: When the Vocabulary Outlives the Data
&lt;/h2&gt;

&lt;p&gt;In early 2023, Jessica Rumbelow and Matthew Watkins went looking at GPT-2 and GPT-3's token embeddings and found a cluster of strange entries: &lt;code&gt;SolidGoldMagikarp&lt;/code&gt;, &lt;code&gt;TheNitromeFan&lt;/code&gt;, &lt;code&gt;cloneembedreportprint&lt;/code&gt;, &lt;code&gt;petertodd&lt;/code&gt;. In their &lt;a href="https://www.lesswrong.com/posts/aPeJE8bSo6rAFoLqg/solidgoldmagikarp-plus-prompt-generation" rel="noopener noreferrer"&gt;LessWrong write-up&lt;/a&gt;, they showed that asking a model to simply repeat these strings produced evasion, insults or substitutions. ChatGPT, for example, repeated " SolidGoldMagikarp" as "distribute." Some of these tokens even made GPT-3 give different answers at temperature 0, which is supposed to be deterministic.&lt;/p&gt;

&lt;p&gt;Their partial explanation goes back to the point about merges. The tokenizer was trained on scraped web text that included Reddit usernames, e-commerce site internals and game logs. The model was trained on more heavily curated text. So strings like &lt;code&gt;SolidGoldMagikarp&lt;/code&gt; (I confirmed it's a single token, #43453, in GPT-2's vocabulary) earned a vocabulary slot but almost never showed up during model training. Their embedding vectors stayed close to wherever they started, and the model doesn't know what to do when it meets one.&lt;/p&gt;

&lt;p&gt;It's not just a GPT-2 curiosity. &lt;a href="https://arxiv.org/abs/2405.05417" rel="noopener noreferrer"&gt;Fishing for Magikarp&lt;/a&gt; (Land and Bartolo, 2024) built automatic detectors that combine tokenizer analysis, weight-based signals and prompting, and found under-trained tokens across a wide range of models. In newer OpenAI vocabularies, " SolidGoldMagikarp" is 5 ordinary tokens (&lt;code&gt;·Solid | Gold | Mag | ik | arp&lt;/code&gt;), but each new tokenizer can bring its own oddities. If you train or fine-tune models, scanning for under-trained tokens is cheap insurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other Tokenizer Families
&lt;/h2&gt;

&lt;p&gt;BPE dominates today's LLMs, but you'll run into two other algorithms, often through one library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPiece&lt;/strong&gt; (developed by Google for BERT) also learns merges bottom-up, but according to the &lt;a href="https://huggingface.co/learn/llm-course/chapter6/6" rel="noopener noreferrer"&gt;Hugging Face course&lt;/a&gt; it scores a pair as its frequency divided by the product of its parts' frequencies, which favors pairs whose parts are rare on their own. It keeps only the final vocabulary and encodes each word by taking the longest piece it knows, then the next. Pieces inside a word carry a &lt;code&gt;##&lt;/code&gt; prefix, as in &lt;code&gt;token ##ization&lt;/code&gt;. (Google never open-sourced its training code, so the course describes a best-guess reconstruction.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unigram LM&lt;/strong&gt;, from Taku Kudo's &lt;a href="https://arxiv.org/abs/1804.10959" rel="noopener noreferrer"&gt;Subword Regularization&lt;/a&gt; (2018), works top-down. It starts with a large candidate vocabulary, scores how much each piece contributes to a probabilistic model of the corpus, and prunes the least useful pieces. Since the same word can be segmented several ways, Kudo trained translation models on &lt;em&gt;sampled&lt;/em&gt; segmentations as a kind of noise, which made them more robust, especially in low-resource and out-of-domain settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SentencePiece&lt;/strong&gt; (&lt;a href="https://arxiv.org/abs/1808.06226" rel="noopener noreferrer"&gt;Kudo and Richardson, 2018&lt;/a&gt;) is a library rather than an algorithm. It implements both BPE and Unigram and trains &lt;em&gt;directly on raw sentences&lt;/em&gt;, with no language-specific pre-splitting. Whitespace becomes the visible symbol &lt;code&gt;▁&lt;/code&gt;, so &lt;code&gt;I saw a girl&lt;/code&gt; becomes &lt;code&gt;▁I ▁saw ▁a ▁girl&lt;/code&gt; (example from the &lt;a href="https://github.com/google/sentencepiece" rel="noopener noreferrer"&gt;SentencePiece README&lt;/a&gt;). That makes it a natural fit for languages that don't put spaces between words, like Japanese and Chinese.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Picking a Vocabulary Size
&lt;/h2&gt;

&lt;p&gt;Why not a million tokens? Every entry costs a row in the input embedding matrix and another in the output layer that scores the next token (unless the two are tied). With 4,096-dimensional vectors, a 200,000-entry vocabulary is about 820 million parameters per matrix. More entries also means fewer training examples per rare token, which is the glitch-token problem again.&lt;/p&gt;

&lt;p&gt;Smaller vocabularies mean longer sequences: attention costs more, the context window fills faster and generation takes more steps. Recent models have moved toward larger vocabularies (50K, then 100K, 128K and 200K) as the embedding table becomes a small share of total parameters. Meta credits Llama 3's 128K tokenizer with "substantially improved model performance."&lt;/p&gt;

&lt;h2&gt;
  
  
  A Cheat Sheet for Engineers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo123vn29f67sg9jwcn1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo123vn29f67sg9jwcn1.png" alt="Common tokenizer gotchas, their causes and practical fixes" width="800" height="639"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Common tokenizer gotchas, their causes and practical fixes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few of these deserve a sentence more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count tokens with the real tokenizer.&lt;/strong&gt; Use &lt;code&gt;tiktoken.encoding_for_model("gpt-4o")&lt;/code&gt; for OpenAI models, or your provider's token-counting endpoint, before you enforce context limits or estimate costs. Character-based estimates are wrong for code, non-English text and JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the boundary at the end of your prompt.&lt;/strong&gt; Because GPT-style tokenizers attach the space to the &lt;em&gt;following&lt;/em&gt; word, " Paris" is a single token. A prompt that ends with a trailing space ("The capital of France is ") leaves a bare space token, and the model then has to continue with "Paris" &lt;em&gt;without&lt;/em&gt; its usual leading space, an unusual sequence it saw less often in training. Trim trailing whitespace from completion-style prompts. The "token healing" technique cited by Singh and Strouse goes further: it backs up a few tokens and lets the model regenerate across the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special tokens are control characters.&lt;/strong&gt; Strings like &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt; map to reserved IDs (100257 in &lt;code&gt;cl100k_base&lt;/code&gt;, 199999 in &lt;code&gt;o200k_base&lt;/code&gt;) that mark document and message boundaries. By default tiktoken refuses to encode them from ordinary text: &lt;code&gt;encode("hi &amp;lt;|endoftext|&amp;gt;")&lt;/code&gt; raises an error. Keep that protection on, so pasted text can't forge a document or message boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLMs read integers, not letters.&lt;/strong&gt; A tokenizer turns text into IDs before the model runs, and anything inside a token, like its spelling, is invisible unless the model memorized it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BPE builds the vocabulary by merging the most frequent adjacent pairs&lt;/strong&gt;, and byte-level BPE guarantees that any input can be encoded with no "unknown" token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization explains classic failure modes:&lt;/strong&gt; letter counting, multi-digit arithmetic (75.6% vs 97.8% for GPT-3.5 depending only on commas), higher costs for some languages (up to 15× longer encodings) and glitch tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix is usually to change the representation or hand off the work:&lt;/strong&gt; spell words out, format numbers deliberately, and call code for anything that must be exact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure with the actual tokenizer.&lt;/strong&gt; "4 characters per token" is a rough English-only average, not a budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading (sources used)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sennrich, Haddow and Birch, &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Neural Machine Translation of Rare Words with Subword Units&lt;/a&gt; (ACL 2016), the paper that brought BPE to NLP&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, &lt;a href="https://huggingface.co/learn/llm-course/chapter6/5" rel="noopener noreferrer"&gt;Byte-Pair Encoding tokenization&lt;/a&gt;, with a worked example and a from-scratch implementation&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; and its &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;encoding definitions&lt;/a&gt; (pre-split regexes, vocabularies, special tokens)&lt;/li&gt;
&lt;li&gt;Singh and Strouse, &lt;a href="https://arxiv.org/abs/2402.14903" rel="noopener noreferrer"&gt;Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs&lt;/a&gt; (2024)&lt;/li&gt;
&lt;li&gt;Petrov et al., &lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Language Model Tokenizers Introduce Unfairness Between Languages&lt;/a&gt; (2023)&lt;/li&gt;
&lt;li&gt;Rumbelow and Watkins, &lt;a href="https://www.lesswrong.com/posts/aPeJE8bSo6rAFoLqg/solidgoldmagikarp-plus-prompt-generation" rel="noopener noreferrer"&gt;SolidGoldMagikarp (plus, prompt generation)&lt;/a&gt; (2023)&lt;/li&gt;
&lt;li&gt;Land and Bartolo, &lt;a href="https://arxiv.org/abs/2405.05417" rel="noopener noreferrer"&gt;Fishing for Magikarp: Automatically Detecting Under-trained Tokens in Large Language Models&lt;/a&gt; (2024)&lt;/li&gt;
&lt;li&gt;Kudo, &lt;a href="https://arxiv.org/abs/1804.10959" rel="noopener noreferrer"&gt;Subword Regularization&lt;/a&gt; (ACL 2018), and Kudo and Richardson, &lt;a href="https://arxiv.org/abs/1808.06226" rel="noopener noreferrer"&gt;SentencePiece&lt;/a&gt; (2018)&lt;/li&gt;
&lt;li&gt;Meta AI, &lt;a href="https://ai.meta.com/blog/meta-llama-3/" rel="noopener noreferrer"&gt;Introducing Meta Llama 3&lt;/a&gt; (128K vocabulary, up to 15% fewer tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Try it yourself: &lt;code&gt;pip install tiktoken&lt;/code&gt;, then &lt;code&gt;tiktoken.get_encoding("o200k_base").encode(" strawberry")&lt;/code&gt; and see what you get.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ragmehta.substack.com" rel="noopener noreferrer"&gt;AI Engineering Illustrated&lt;/a&gt;. Subscribe for new deep dives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>nlp</category>
    </item>
    <item>
      <title>How LLM Inference Serving Works: KV Cache, PagedAttention and Continuous Batching</title>
      <dc:creator>AI Engineering Illustrated</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:21:24 +0000</pubDate>
      <link>https://dev.to/aiengillustrated/how-llm-inference-serving-works-kv-cache-pagedattention-and-continuous-batching-2hbk</link>
      <guid>https://dev.to/aiengillustrated/how-llm-inference-serving-works-kv-cache-pagedattention-and-continuous-batching-2hbk</guid>
      <description>&lt;p&gt;&lt;em&gt;Why serving a model is mostly a memory-management problem, and the operating-systems idea that fixed it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Training a large language model gets the headlines, but &lt;strong&gt;inference&lt;/strong&gt; is where most of the compute bill goes in production. Every chat message, every agent step and every autocomplete is a request for a GPU to generate tokens. The difference between a naive serving stack and a good one isn't 10%. The vLLM team reported up to &lt;strong&gt;24×&lt;/strong&gt; higher throughput than serving with Hugging Face Transformers when they launched in June 2023.&lt;/p&gt;

&lt;p&gt;Where does a gap that large come from? Not from faster matrix multiplication. It comes from three things most application developers never see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How the &lt;strong&gt;KV cache&lt;/strong&gt; is stored in GPU memory&lt;/li&gt;
&lt;li&gt;How requests are &lt;strong&gt;batched&lt;/strong&gt; together&lt;/li&gt;
&lt;li&gt;How work is &lt;strong&gt;scheduled&lt;/strong&gt; between the two phases of generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this issue, we'll look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why generation has two very different phases, prefill and decode&lt;/li&gt;
&lt;li&gt;What the KV cache is, and how large it really gets (with exact numbers for Llama 3.1 8B)&lt;/li&gt;
&lt;li&gt;How &lt;strong&gt;PagedAttention&lt;/strong&gt; borrowed virtual memory from operating systems to stop wasting most of that memory&lt;/li&gt;
&lt;li&gt;Why &lt;strong&gt;continuous batching&lt;/strong&gt; replaced static batching&lt;/li&gt;
&lt;li&gt;How &lt;strong&gt;prefix caching&lt;/strong&gt; and &lt;strong&gt;chunked prefill&lt;/strong&gt; squeeze out more performance, and what that means for how you write prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two Phases: Prefill and Decode
&lt;/h2&gt;

&lt;p&gt;An LLM generates text one token at a time. Each new token depends on all the previous ones. Under the hood, a single request goes through two phases with very different performance profiles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifjb20g02xp8lsw36uh6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifjb20g02xp8lsw36uh6.png" alt="Prefill processes the prompt in parallel; decode produces one token per step." width="800" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefill&lt;/strong&gt; processes the entire prompt in one forward pass. All prompt tokens go through the model in parallel, which means large matrix multiplications that keep the GPU's compute units busy. Prefill is &lt;strong&gt;compute-bound&lt;/strong&gt;. Its output is the first generated token, so prefill time largely sets &lt;strong&gt;time-to-first-token (TTFT)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decode&lt;/strong&gt; generates every token after that, one forward pass per token. Each step processes only one new token per request, but to compute attention it has to read the keys and values of every previous token. So each step loads the model's weights plus the request's entire cached history from GPU memory, does relatively little math with it, and produces one token. Decode is &lt;strong&gt;memory-bound&lt;/strong&gt;: the bottleneck is how fast bytes move from GPU memory (HBM), not how fast the GPU computes. vLLM's own tuning docs describe the split exactly this way: "compute-bound (prefill) and memory-bound (decode)".&lt;/p&gt;

&lt;p&gt;This asymmetry drives almost every design decision that follows. Since decode is limited by memory bandwidth, the main way to get more out of a GPU is to &lt;strong&gt;decode many requests at once&lt;/strong&gt;. Loading the weights once and using them for 64 requests is far better than loading them 64 times. Batch size is the lever, and what limits batch size is memory, specifically the KV cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the KV Cache Is, and How Big It Gets
&lt;/h2&gt;

&lt;p&gt;In a transformer's attention layers, each token is projected into a &lt;strong&gt;query&lt;/strong&gt;, a &lt;strong&gt;key&lt;/strong&gt; and a &lt;strong&gt;value&lt;/strong&gt;. When generating token &lt;em&gt;n&lt;/em&gt;, the model compares token &lt;em&gt;n&lt;/em&gt;'s query against the keys of tokens 1…&lt;em&gt;n−1&lt;/em&gt; and takes a weighted mix of their values. Those keys and values never change once computed, so recomputing them at every step would be wasteful. Instead, inference engines store them in the &lt;strong&gt;KV cache&lt;/strong&gt;. Prefill fills the cache for the prompt, and each decode step appends one more token's K and V.&lt;/p&gt;

&lt;p&gt;How much memory is that? It's a simple product:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;KV bytes per token = 2 (K and V) × layers × KV heads × head dimension × bytes per value&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gyd6mmfal9q0fnt0mi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gyd6mmfal9q0fnt0mi1.png" alt="KV cache size for Llama 3.1 8B: 128 KiB per token." width="800" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plugging in the published &lt;code&gt;config.json&lt;/code&gt; for &lt;strong&gt;Llama 3.1 8B&lt;/strong&gt;: 32 layers, 8 key/value heads (the model uses grouped-query attention, so its 32 query heads share 8 KV heads), head dimension 128, and bf16 values at 2 bytes each:&lt;/p&gt;

&lt;p&gt;2 × 32 × 8 × 128 × 2 = &lt;strong&gt;131,072 bytes = 128 KiB per token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds small until you multiply by context length:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1K tokens → 128 MiB&lt;/li&gt;
&lt;li&gt;8K tokens → 1 GiB&lt;/li&gt;
&lt;li&gt;32K tokens → 4 GiB&lt;/li&gt;
&lt;li&gt;128K tokens (the model's maximum context) → &lt;strong&gt;16 GiB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model's weights are about 8 billion parameters × 2 bytes ≈ 16 GB. &lt;strong&gt;A single conversation at full context needs about as much memory as the entire model.&lt;/strong&gt; The weights are shared by every request. The KV cache is per request, and it grows with every token generated.&lt;/p&gt;

&lt;p&gt;(Grouped-query attention is itself a KV-cache optimization. With 32 KV heads instead of 8, every number above would be four times larger.)&lt;/p&gt;

&lt;p&gt;The vLLM launch post gives a sense of scale for an older model: the KV cache can take up to &lt;strong&gt;1.7 GB for a single sequence in LLaMA-13B&lt;/strong&gt;. It also describes the cache as &lt;strong&gt;dynamic&lt;/strong&gt;: "its size depends on the sequence length, which is highly variable and unpredictable." That unpredictability is the real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Fragmentation
&lt;/h2&gt;

&lt;p&gt;When you start generating, you don't know whether the answer will be 20 tokens or 2,000. Early serving systems handled this the obvious way. They reserved a &lt;strong&gt;contiguous&lt;/strong&gt; chunk of GPU memory for each request, big enough for the maximum possible length.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo2kdumylq5ukuagri1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo2kdumylq5ukuagri1l.png" alt="PagedAttention: fixed-size blocks mapped by a block table, like virtual memory." width="800" height="1007"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The top half of the diagram shows what goes wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Over-reservation.&lt;/strong&gt; A request that ends after 200 tokens still held space for 2,048. That reserved but unused memory can't go to anyone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal fragmentation.&lt;/strong&gt; Even within a reservation, the unused tail sits idle the whole time the request is running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External fragmentation.&lt;/strong&gt; Contiguous allocations of different sizes leave gaps between them that are too small to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The vLLM team measured this: &lt;strong&gt;existing systems wasted 60%–80% of KV cache memory&lt;/strong&gt; due to fragmentation and over-reservation. Since memory caps batch size, and batch size caps decode throughput, most of the GPU's potential was being thrown away.&lt;/p&gt;

&lt;h2&gt;
  
  
  PagedAttention: Virtual Memory for the KV Cache
&lt;/h2&gt;

&lt;p&gt;Operating systems solved this exact problem decades ago. Programs want large contiguous address spaces, but physical memory is fragmented. The OS splits memory into fixed-size &lt;strong&gt;pages&lt;/strong&gt;, gives each process a contiguous &lt;em&gt;virtual&lt;/em&gt; address space, and keeps a &lt;strong&gt;page table&lt;/strong&gt; mapping virtual pages to wherever they physically sit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagedAttention&lt;/strong&gt;, introduced in the vLLM paper (SOSP 2023, arXiv:2309.06180), applies the same idea to the KV cache. In the authors' words: "one can think of blocks as pages, tokens as bytes, and sequences as processes."&lt;/p&gt;

&lt;p&gt;The bottom half of the diagram shows how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;KV memory is split into fixed-size &lt;strong&gt;blocks&lt;/strong&gt;, each holding the keys and values for a fixed number of tokens (configurable in vLLM with &lt;code&gt;--block-size&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Each request sees a sequence of &lt;strong&gt;logical blocks&lt;/strong&gt; that are contiguous from its own point of view.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;block table&lt;/strong&gt; maps each logical block to a &lt;strong&gt;physical block&lt;/strong&gt; anywhere in GPU memory. Physical blocks don't need to be adjacent.&lt;/li&gt;
&lt;li&gt;Physical blocks are &lt;strong&gt;allocated on demand&lt;/strong&gt;, only when a request's current block fills up.&lt;/li&gt;
&lt;li&gt;The attention kernel is rewritten to follow the block table, gathering each request's K and V from wherever its blocks live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The payoff: &lt;strong&gt;memory waste only happens in a request's last, partially filled block.&lt;/strong&gt; The vLLM post reports this results in "a mere waste of under 4%". Nearly all KV memory now holds real data, so far more requests fit in a batch. The paper's headline result is that vLLM "improves the throughput of popular LLMs by &lt;strong&gt;2-4×&lt;/strong&gt; with the same level of latency" compared with FasterTransformer and Orca, with bigger gains for longer sequences, larger models and more complex decoding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing comes for free
&lt;/h3&gt;

&lt;p&gt;Once memory is indirected through a block table, two requests can point at the &lt;strong&gt;same&lt;/strong&gt; physical block. This matters for decoding strategies that create several outputs from one prompt, like parallel sampling ("give me 4 candidate answers") or beam search. The prompt's KV blocks are stored once and shared, with reference counts. A block is copied only when one branch needs to write to it (&lt;strong&gt;copy-on-write&lt;/strong&gt;, another OS technique). The vLLM team reports this cuts memory usage of these methods "by up to 55%", which can mean "up to 2.2× improvement in throughput."&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Batching
&lt;/h2&gt;

&lt;p&gt;Freeing memory only helps if the scheduler actually fills it with work. The classic approach, &lt;strong&gt;static&lt;/strong&gt; or request-level batching, doesn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgdudf8b0dun214q934z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgdudf8b0dun214q934z.png" alt="Static batching waits for the longest request; continuous batching re-forms the batch every step." width="800" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With static batching, the server collects a batch of requests and runs them together until &lt;strong&gt;all&lt;/strong&gt; of them are done. But generation lengths vary wildly. The Orca paper (OSDI '22) describes the problem precisely: with request-level scheduling, "requests that have finished earlier than other requests in a batch cannot return to the client, while newly arrived requests have to wait until the current batch completely finishes."&lt;/p&gt;

&lt;p&gt;Orca's fix was &lt;strong&gt;iteration-level scheduling&lt;/strong&gt;, now usually called &lt;strong&gt;continuous batching&lt;/strong&gt;. The scheduler re-forms the batch at &lt;strong&gt;every model iteration&lt;/strong&gt;, meaning every decode step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A request that just produced its final token leaves the batch immediately and its result is returned.&lt;/li&gt;
&lt;li&gt;A newly arrived request joins at the very next iteration.&lt;/li&gt;
&lt;li&gt;The batch stays full, so the GPU stays busy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Continuous batching and PagedAttention reinforce each other. Continuous batching constantly admits and evicts requests of unpredictable length, which is exactly the workload that fragments contiguous memory. Paged blocks make admitting a new request cheap: you just need a few free blocks, not a large contiguous region.&lt;/p&gt;

&lt;h3&gt;
  
  
  When memory runs out anyway
&lt;/h3&gt;

&lt;p&gt;Even with paging, a burst of long requests can fill the KV cache. vLLM then &lt;strong&gt;preempts&lt;/strong&gt; some requests to free their blocks. In vLLM V1 the default preemption mode is &lt;strong&gt;recompute&lt;/strong&gt;: the preempted request's KV is dropped and rebuilt later, rather than swapped to CPU memory, "as recomputation has lower overhead in the V1 architecture." If your logs show preemption warnings, the docs suggest giving vLLM more memory (&lt;code&gt;gpu_memory_utilization&lt;/code&gt;, which defaults to 0.92), reducing &lt;code&gt;max_num_seqs&lt;/code&gt; or &lt;code&gt;max_num_batched_tokens&lt;/code&gt;, or sharding the model across more GPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefix Caching and Chunked Prefill
&lt;/h2&gt;

&lt;p&gt;Two more techniques complete the picture. The first one directly affects how you should write prompts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkruyu2hbtf4pdw06le1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkruyu2hbtf4pdw06le1t.png" alt="Prefix caching reuses shared KV blocks; chunked prefill schedules decodes first." width="800" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic prefix caching
&lt;/h3&gt;

&lt;p&gt;Real traffic is highly repetitive at the start. Every request to an agent typically begins with the same system prompt, the same tool definitions, maybe the same retrieved documents. Running prefill on those identical tokens over and over is pure waste.&lt;/p&gt;

&lt;p&gt;With prefix caching, vLLM keeps the KV blocks of previous requests and reuses them. The design docs explain how a match is found: vLLM hashes "each kv-cache block by the tokens in the block and the tokens in the prefix before the block." Because the hash includes everything before the block, a block matches only if the &lt;strong&gt;entire prefix&lt;/strong&gt; up to it is identical. A lookup table maps hashes to cached physical blocks, so a new request whose first 3,000 tokens match an earlier one skips straight to prefilling only the new suffix.&lt;/p&gt;

&lt;p&gt;Cached blocks no longer in use aren't freed immediately. They stay available for reuse and are evicted in &lt;strong&gt;LRU&lt;/strong&gt; order when memory is needed. For multi-tenant deployments, vLLM supports an optional per-request &lt;code&gt;cache_salt&lt;/code&gt; that is mixed into the first block's hash, so different tenants can't reuse, or probe, each other's cached prefixes.&lt;/p&gt;

&lt;p&gt;The docs note that prefix caching is "widely used by many public endpoints (e.g., OpenAI, Anthropic, etc.)". This is the mechanism behind the discounted "cached input" pricing those APIs offer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical lesson for prompt design:&lt;/strong&gt; put stable content first and variable content last. System prompt, then tool definitions, then long reference documents, then the user's actual question. Changing a single token early in the prompt (a timestamp in the system prompt, say) breaks the hash chain from that point on and forces a full prefill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chunked prefill
&lt;/h3&gt;

&lt;p&gt;Remember that prefill is compute-heavy and decode is memory-bound. If a 10,000-token prompt arrives while 60 users are streaming responses, a naive scheduler runs that huge prefill as one step, and every streaming user sees a stall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunked prefill&lt;/strong&gt; splits large prefills into pieces and mixes them into steps alongside decodes. vLLM's docs describe the V1 policy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each step has a token budget, &lt;code&gt;max_num_batched_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The scheduler &lt;strong&gt;prioritizes decode requests&lt;/strong&gt;: it "batches all pending decode requests before scheduling any prefill operations."&lt;/li&gt;
&lt;li&gt;Any remaining budget goes to pending prefills. A prefill that doesn't fit "automatically" gets chunked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This improves inter-token latency, because streams don't stall, and GPU utilization, because compute-bound and memory-bound work share each step. The budget is a tuning knob. Smaller values (the docs give 2048 as an example) favor smooth streaming. Larger values favor time-to-first-token, and the docs recommend going above 8192 for throughput on large GPUs. In vLLM V1, chunked prefill "is enabled by default whenever possible."&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together: The Life of a Request in vLLM
&lt;/h2&gt;

&lt;p&gt;Here's what happens to one request in a modern serving engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The request arrives and is tokenized.&lt;/li&gt;
&lt;li&gt;The scheduler checks the &lt;strong&gt;prefix cache&lt;/strong&gt;. Matching leading blocks are reused, and only the remainder needs prefill.&lt;/li&gt;
&lt;li&gt;At the next iteration, the scheduler admits the request if enough &lt;strong&gt;free KV blocks&lt;/strong&gt; exist, preempting another request if necessary.&lt;/li&gt;
&lt;li&gt;Its prefill runs, chunked if it's long, alongside other requests' decode steps, within the token budget.&lt;/li&gt;
&lt;li&gt;Each decode step appends one token's K and V into the request's current block. A new physical block is allocated only when that block fills.&lt;/li&gt;
&lt;li&gt;When the request emits its stop token, it leaves the batch immediately. Its blocks' reference counts drop, and they become cached-but-evictable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step targets the same scarce resource: &lt;strong&gt;GPU memory for KV cache&lt;/strong&gt;, and keeping the GPU busy with as many requests as that memory allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means If You Don't Run GPUs
&lt;/h2&gt;

&lt;p&gt;Even if you only call hosted APIs, these mechanics show up on your bill and in your latency charts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long prompts cost twice:&lt;/strong&gt; compute at prefill (TTFT), and memory for every decode step that follows. Trim context you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt structure affects cost.&lt;/strong&gt; Stable prefixes hit the cache. Dynamic content at the top of the prompt defeats it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output length affects throughput more than you'd think.&lt;/strong&gt; Every generated token is a full decode step holding KV memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTFT and tokens-per-second are different metrics&lt;/strong&gt; driven by different phases. A provider can be strong on one and weak on the other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inference has two phases:&lt;/strong&gt; prefill (parallel, compute-bound, sets TTFT) and decode (one token per step, memory-bound).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The KV cache is the bottleneck.&lt;/strong&gt; For Llama 3.1 8B it's 128 KiB per token, so 16 GiB at a full 128K context, about the size of the model itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contiguous allocation wasted 60–80%&lt;/strong&gt; of KV memory. &lt;strong&gt;PagedAttention&lt;/strong&gt; uses fixed-size blocks and a block table, like OS virtual memory, cutting waste to under 4% and delivering 2–4× throughput in the paper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous batching&lt;/strong&gt; (Orca's iteration-level scheduling) re-forms the batch every step, so finished requests leave and new ones join immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix caching&lt;/strong&gt; reuses KV blocks for identical prefixes via chained block hashes, so put stable prompt content first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunked prefill&lt;/strong&gt; schedules decodes first and splits long prompts, so one big prompt doesn't stall everyone else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading (sources used)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention" (arXiv:2309.06180): &lt;a href="https://arxiv.org/abs/2309.06180" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2309.06180&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM launch post, "vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention" (June 20, 2023): &lt;a href="https://blog.vllm.ai/2023/06/20/vllm.html" rel="noopener noreferrer"&gt;https://blog.vllm.ai/2023/06/20/vllm.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Yu et al., "Orca: A Distributed Serving System for Transformer-Based Generative Models" (OSDI '22): &lt;a href="https://www.usenix.org/conference/osdi22/presentation/yu" rel="noopener noreferrer"&gt;https://www.usenix.org/conference/osdi22/presentation/yu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM docs, Automatic Prefix Caching design: &lt;a href="https://docs.vllm.ai/en/latest/design/prefix_caching.html" rel="noopener noreferrer"&gt;https://docs.vllm.ai/en/latest/design/prefix_caching.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM docs, Optimization and Tuning (preemption, chunked prefill): &lt;a href="https://docs.vllm.ai/en/latest/configuration/optimization.html" rel="noopener noreferrer"&gt;https://docs.vllm.ai/en/latest/configuration/optimization.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM docs, Engine arguments: &lt;a href="https://docs.vllm.ai/en/latest/configuration/engine_args.html" rel="noopener noreferrer"&gt;https://docs.vllm.ai/en/latest/configuration/engine_args.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Llama 3.1 8B &lt;code&gt;config.json&lt;/code&gt; (mirror of Meta's published config): &lt;a href="https://huggingface.co/unsloth/Meta-Llama-3.1-8B/blob/main/config.json" rel="noopener noreferrer"&gt;https://huggingface.co/unsloth/Meta-Llama-3.1-8B/blob/main/config.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published on &lt;a href="https://ragmehta.substack.com" rel="noopener noreferrer"&gt;AI Engineering Illustrated&lt;/a&gt;. Subscribe for new deep dives.&lt;/p&gt;

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