<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://shekkari1999.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://shekkari1999.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-08-14T23:18:38+00:00</updated><id>https://shekkari1999.github.io/feed.xml</id><title type="html">Akhil Shekkari</title><subtitle>AI engineer focused on LLM inference, serving, and agent systems. </subtitle><entry><title type="html">Lessons I Learnt from Building vLLM Internals</title><link href="https://shekkari1999.github.io/blog/2026/mini-vllm-lessons/" rel="alternate" type="text/html" title="Lessons I Learnt from Building vLLM Internals"/><published>2026-06-01T09:00:00+00:00</published><updated>2026-06-01T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2026/mini-vllm-lessons</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2026/mini-vllm-lessons/"><![CDATA[<p> I had written about KV cache, PagedAttention, and continuous batching before. I could explain them in a post. I still did not trust my own mental model until I tried to rebuild the pieces myself. </p> <p> So I built <strong>mini-vllm</strong>: a small inference loop around <a href="https://huggingface.co/meta-llama/Llama-2-7b-hf" target="_blank" rel="noopener">Llama-2-7B</a> on a single GPU. Scheduler, block allocator, paged KV, generation loop. Not a production server, just enough to see how the ideas connect. <a href="https://github.com/shekkari1999/minivllm" target="_blank" rel="noopener">Code is on GitHub</a> if you want the implementation. </p> <h2>What an inference engine actually is</h2> <p> From the outside it looks like one model call. Inside it is three boring layers plus weights: </p> <ul> <li><strong>Per-request state</strong> on the CPU: prompt, tokens generated so far, when to stop, which memory blocks you own.</li> <li><strong>Memory management</strong>: a fixed pool of KV blocks on the GPU, handed out like parking spots.</li> <li><strong>Scheduling</strong>: who runs this step, who waits, who just finished and must give blocks back.</li> </ul> <p> The transformer math is the easy part to read about. Most of my confusion was in those three layers talking to each other. </p> <h2>PagedAttention, in one picture</h2> <p> Naive serving often reserves a long KV buffer per request. Short prompts waste most of it, so you fit fewer concurrent users. </p> <p> PagedAttention is the opposite idea: allocate one big KV tensor up front, then give each request a short list of block ids. Token 47 does not mean "grow a tensor." It means "block 2, slot 15 in the shared buffer." </p> <p> The allocator is just bookkeeping on those ids. The GPU tensor never changes shape during inference. That was the click for me: all the "paging" drama is CPU-side lists and integers, not new CUDA allocations every token. </p> <h2>Things I had backwards at first</h2> <p> <strong>I thought the block table stored vectors.</strong> It stores handles into the pool. The vectors live in one place; the table is a map. </p> <p> <strong>I mixed up fleet metrics with request state.</strong> Before writing code I listed SLOs and counts. Step six of generation only needs identity, full output history, stop rules, and the prompt. </p> <p> <strong>I reordered the scheduler wrong.</strong> If you grow block tables before you drop finished sequences, you can allocate for a request that already ended and free it in the same pass. Small bug, very vLLM-shaped lesson: lifecycle order matters as much as the algorithm on paper. </p> <p> <strong>I treated prefill and decode as different models.</strong> Same weights. Different input shapes. Decode is one new token with a position index in the full sequence, not "position 0 in this one-row tensor." I lost time on RoPE before I believed that. </p> <p> <strong>I underestimated memory layout vs math.</strong> Growing KV with concat-every-step means allocate, copy, free in the hot loop. Writing into a fixed slot is why serving engines feel so much faster in practice. The gap is often memory traffic, not a smarter softmax. </p> <h2>What surprised me</h2> <p> Papers teach the what. Building taught the why behind small choices. </p> <ul> <li>Refcounts on blocks only make sense once you imagine two requests sharing a prefix later.</li> <li>Most bugs were state machines and indexing, not attention.</li> <li>File layout followed dependencies: engine owns everything, nothing imports the engine. I did not design that upfront; it fell out of "who creates whom at startup."</li> </ul> <h2>If you want to try this yourself</h2> <p> Start from the <a href="https://arxiv.org/abs/2309.06180" target="_blank" rel="noopener">PagedAttention paper</a> with a sketch of logical tokens vs physical blocks. Get one sequence generating before you turn on batching. Then read <a href="https://www.aleksagordic.com/blog/vllm" target="_blank" rel="noopener">Inside vLLM</a> or peek at <a href="https://github.com/GeeeekExplorer/nano-vllm" target="_blank" rel="noopener">nano-vllm</a> once your own loop works. Comparisons land better when you have something to compare against. </p> <p> mini-vllm is my version of that exercise. The repo has benchmarks and the full wiring; this post is only the flavour I wish I had before I opened the editor. </p>]]></content><author><name></name></author><category term="engineering"/><category term="inference"/><category term="systems"/><category term="llm"/><summary type="html"><![CDATA[What building a small inference engine revealed about paged KV cache, scheduling, and state.]]></summary></entry><entry><title type="html">Context in LLMs: What Determines It, What It Costs, and What Actually Works</title><link href="https://shekkari1999.github.io/blog/2026/improving-context/" rel="alternate" type="text/html" title="Context in LLMs: What Determines It, What It Costs, and What Actually Works"/><published>2026-02-06T09:00:00+00:00</published><updated>2026-02-06T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2026/improving-context</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2026/improving-context/"><![CDATA[<h2>1. What Determines Context Length</h2> <p> When people say a model "supports 128K context," that sounds like a single setting you can crank up. It's not. Context length is actually limited by three things at once: how the model tracks token position, how expensive attention gets as sequences grow, and how much memory you need to store past tokens during inference. Each one puts a ceiling on how far you can go. </p> <h3>Constraint 1: Positional Encoding Scheme</h3> <p> Transformers have no built-in sense of order. Self-attention is permutation-invariant, meaning it treats "the cat sat" the same as "sat cat the" unless you explicitly tell it which token came first. Positional encodings inject that order. The scheme you pick dictates the ceiling. </p> <div class="quantization-table"> <table> <thead> <tr> <th>Scheme</th> <th>How It Works</th> <th>Limit</th> </tr> </thead> <tbody> <tr> <td><strong>Learned Absolute</strong> (original GPT)</td> <td>Learns an embedding vector for positions 0..N-1</td> <td>Hard ceiling at N. Position 4097 literally doesn't exist.</td> </tr> <tr> <td><strong>Sinusoidal</strong> (original Transformer)</td> <td>Fixed sin/cos at different frequencies</td> <td>Theoretically infinite, but untested positions degrade</td> </tr> <tr> <td><strong>RoPE</strong> (LLaMA, Qwen, most modern LLMs)</td> <td>Encodes position as rotation angle in embedding space</td> <td>Theoretically extrapolable, but rotations at unseen angles are out-of-distribution</td> </tr> <tr> <td><strong>ALiBi</strong> (BLOOM)</td> <td>Adds linear bias to attention scores based on distance</td> <td>Better extrapolation, but penalizes long-range attention by design</td> </tr> </tbody> </table> </div> <h4>RoPE: The Dominant Approach (and Its Limitation)</h4> <p> Most modern models use <strong>Rotary Position Embedding (RoPE)</strong>. For each dimension pair (i), the rotation angle at position m is: </p> <div class="code-example"> &theta;_i(m) = m &times; base^(-2i/d) where base = 10000 (typically), d = head dimension</div> <p> <strong>The problem:</strong> If you train on positions 0 to 4096, the model has only ever seen rotation angles in that range. Position 8000 produces rotations the model has never encountered. It's like teaching someone a 12-hour clock then asking them to read a 24-hour clock. The mechanism is the same but the values are foreign. This is a <strong>distribution shift</strong>, and the model's output degrades unpredictably. </p> <h3>Constraint 2: Attention's Quadratic Cost</h3> <p> Self-attention computes pairwise scores between <strong>every</strong> token pair: </p> <div class="code-example"> Attention(Q, K, V) = softmax(QK^T / &radic;d_k) V QK^T matrix is n &times; n where n = sequence length Memory: O(n&sup2;) Compute: O(n&sup2; &times; d)</div> <div class="quantization-table"> <table> <thead> <tr> <th>From &rarr; To</th> <th>Token Increase</th> <th>Attention Cost Increase</th> </tr> </thead> <tbody> <tr> <td>4K &rarr; 8K</td> <td>2x</td> <td><strong>4x</strong></td> </tr> <tr> <td>4K &rarr; 32K</td> <td>8x</td> <td><strong>64x</strong></td> </tr> <tr> <td>4K &rarr; 128K</td> <td>32x</td> <td><strong>1,024x</strong></td> </tr> <tr> <td>4K &rarr; 1M</td> <td>250x</td> <td><strong>62,500x</strong></td> </tr> </tbody> </table> </div> <p> This is the fundamental reason you can't "just make context longer." The cost grows with the <strong>square</strong> of the length. </p> <h3>Constraint 3: KV Cache at Inference</h3> <p> At inference, you store Key and Value vectors for every past token, every layer. For a 70B model with GQA (8 KV heads): </p> <div class="code-example"> Per token KV cost: = num_layers &times; 2(K,V) &times; num_kv_heads &times; head_dim &times; 2 bytes(fp16) = 80 &times; 2 &times; 8 &times; 128 &times; 2 &asymp; 327 KB per token At different context lengths: 4K context: &rarr; 1.3 GB (manageable) 32K context: &rarr; 10.5 GB (one GPU) 128K context: &rarr; 42 GB (needs multiple GPUs JUST for KV cache) 1M context: &rarr; 327 GB (impossible on current hardware for one request)</div> <div class="note-block"> <strong>Important:</strong> This is <strong>per request</strong>. A serving system handling 100 concurrent users at 128K context needs 4.2 TB just for KV cache, before any model weights. </div> <h3>Why You Can't "Just Extend" the Context</h3> <h4>Problem 1: Training Distribution Mismatch (the deepest reason)</h4> <p> The model learns attention patterns from training data. If most training documents are 2-8K tokens: </p> <ul> <li>It learns "the answer is usually within a few thousand tokens of the question"</li> <li>It learns attention heads that specialize in <strong>local</strong> patterns</li> <li>It <strong>never practices</strong> retrieving a fact from 50K tokens away</li> </ul> <p> Even if you architecturally support 128K, the model hasn't learned <strong>when and how</strong> to attend across that distance. This is a learned behavior problem, not an architecture problem. </p> <h4>Problem 2: Lost in the Middle (Liu et al., 2023)</h4> <p> Even models with "long context" show a U-shaped retrieval curve: </p> <div class="code-example"> Retrieval accuracy by position of relevant info: Beginning: &block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block; 95% Middle: &block;&block;&block;&block;&block;&block;&block;&block; 40% &larr; catastrophic drop End: &block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block;&block; 90%</div> <p> Softmax attention distributes probability mass. Over long sequences, the middle tokens get starved because they don't benefit from primacy or recency bias. </p> <h4>Problem 3: Effective vs. Claimed Context</h4> <p> A model "supporting" 128K and <strong>effectively using</strong> 128K are different things: </p> <div class="code-example"> Claimed: 128K context Needle retrieval at 128K: ~60% Needle retrieval at 4K: ~99%</div> <p> The spec sheet number is the architectural limit. The effective limit is much lower. </p> <h3>The Fundamental Trilemma</h3> <div class="code-example"> Long Context /\ / \ / \ / \ Quality -------- Efficiency</div> <p>Pick at most two:</p> <ul> <li><strong>Long + Quality</strong> = massive compute (full attention, long training)</li> <li><strong>Long + Efficient</strong> = sparse/approximate attention (loses quality)</li> <li><strong>Quality + Efficient</strong> = short context (what most models do best)</li> </ul> <h2>2. Strategies for Extending and Managing Context</h2> <p> There are two fundamentally different approaches: <strong>make the window bigger</strong> (architectural) or <strong>make the model smarter within the window it has</strong> (behavioral). Both have their place. </p> <h3>Architectural Strategies: Making the Window Bigger</h3> <div class="quantization-table"> <table> <thead> <tr> <th>Technique</th> <th>How It Works</th> <th>Trade-off</th> </tr> </thead> <tbody> <tr> <td><strong>Position Interpolation</strong></td> <td>Scale positions down: pos' = pos &times; (L_train / L_target). So position 8000 &rarr; 4000 (within training range).</td> <td>Reduces positional resolution. Nearby tokens look more similar. Fine distinctions blur.</td> </tr> <tr> <td><strong>NTK-Aware Scaling</strong></td> <td>Scale the RoPE frequency base instead of positions: base' = base &times; scale_factor</td> <td>Better than naive interpolation. Preserves local resolution. Still needs fine-tuning on longer data.</td> </tr> <tr> <td><strong>YaRN</strong></td> <td>Different scaling for different frequency dimensions. High-frequency (local patterns): minimal scaling. Low-frequency (global patterns): aggressive scaling. Plus temperature scaling on attention logits.</td> <td>Best extrapolation quality. Most complex to implement. Still needs some continued training.</td> </tr> <tr> <td><strong>ALiBi</strong></td> <td>Linear distance bias on attention scores. No learned positions at all.</td> <td>Good extrapolation, but linearly penalizes distance, limiting long-range attention by design.</td> </tr> <tr> <td><strong>Sliding Window</strong> (Mistral)</td> <td>Each token only attends to last W tokens (e.g., W=4096).</td> <td>O(n&times;W) instead of O(n&sup2;). But literally cannot retrieve beyond window. Stacking layers creates indirect receptive field, but it's lossy.</td> </tr> <tr> <td><strong>Ring Attention</strong></td> <td>Split sequence across GPUs, pass KV in a ring.</td> <td>Solves memory. Does NOT solve the learning problem. Model still must learn long-range patterns.</td> </tr> <tr> <td><strong>Sparse Attention</strong> (Longformer, BigBird)</td> <td>Local + global + random attention patterns. O(n) cost.</td> <td>Some token pairs never directly attend to each other, so information is lost. Replaced in practice by Flash Attention.</td> </tr> <tr> <td><strong>Flash Attention</strong></td> <td>Exact same math as standard attention, but tiled computation fits in GPU SRAM. O(n) memory, O(n&sup2;) compute.</td> <td><strong>No quality loss.</strong> Solves the memory problem. Does NOT reduce compute. This is what everyone actually uses.</td> </tr> <tr> <td><strong>GQA</strong> (Grouped Query Attention)</td> <td>Share KV heads across query heads (e.g., 64 query heads &rarr; 8 KV heads).</td> <td>Reduces KV cache by 8x. Minimal quality loss. Standard in all modern models (Llama 3, Qwen, Mistral).</td> </tr> </tbody> </table> </div> <div class="note-block"> <strong>Key distinction:</strong> Flash Attention is NOT sparse attention. Sparse attention drops connections (quality loss). Flash Attention computes exact full attention but tiles the computation for memory efficiency (no quality loss). Don't confuse them. </div> <h3>Behavioral Strategies: Making the Model Smarter Within Its Window</h3> <p> Instead of making the window bigger, teach the model to <strong>use its existing window more intelligently</strong>. These are agent-level strategies. </p> <h4>Strategy 1: Surgical Retrieval. Read functions, not files</h4> <div class="code-example"> Dumb: Read all of user_service.py (2000 lines) &rarr; ~8K tokens Smart: Read lines 145-178 (the one relevant function) &rarr; ~200 tokens 40x reduction</div> <p> The agent already knows (from the stack trace or grep) which function matters. Reading the whole file dumps 1800 lines of noise into context that competes for attention with the 30 lines that matter. </p> <h4>Strategy 2: Context Eviction. Proactively discard stale information</h4> <div class="code-example"> Turn 3: Read models/user.py to understand schema &rarr; 800 tokens in context Turn 5: Already used that info to write the fix Current: Keep the full file verbatim forever &rarr; 800 tokens wasted Smart: Compress to "User model: id, email_address, created_at" &rarr; 20 tokens 40x reduction</div> <p> After you've acted on information, you rarely need it verbatim. A summary preserves the decision-relevant facts while freeing context for new information. </p> <h4>Strategy 3: Structured Memory. Notes over verbatim copies</h4> <div class="code-example"> Current (raw context): Full file stored in conversation history &rarr; 2000 tokens per file Degrades as it drifts further from attention window Optimized (structured scratchpad): { "file": "models/user.py", "facts": ["email_address: str", "uses SQLAlchemy", "has created_at"], "re_read_lines": "145-150" &larr; pointer to re-read if needed } &rarr; 30 tokens, and exact lines are one tool call away</div> <h4>Strategy 4: Plan Before Reading. Avoid wrong leads entirely</h4> <div class="code-example"> Dumb: "auth broken" &rarr; reads auth.py, user.py, session.py, middleware.py, config.py, database.py &rarr; discovers only auth.py + user.py mattered Cost: 6 file reads &times; ~2K = 12K tokens, 4 were wasted Smart: "auth broken" &rarr; reads stack trace (200 tokens) &rarr; thinks "crash is in login() calling get_user(), need those two files only" &rarr; reads 2 files Cost: 2 file reads &times; ~2K = 4K tokens, 0 wasted</div> <p> 200 tokens of planning saves 8K tokens of unnecessary reads. </p> <h4>Strategy 5: Compaction as a Tool</h4> <p> Compaction doesn't have to be an external system decision. You can give the agent a <code>compact()</code> tool and let it <strong>learn when to use it</strong> through reinforcement learning: </p> <div class="code-example"> Available tools: search(query) &rarr; search the web calculator(expr) &rarr; compute math execute_code(code) &rarr; run Python compact(context) &rarr; summarize working memory, free up context space extract(context, key) &rarr; pull specific info from long text remember(key, value) &rarr; save a fact to persistent memory</div> <p> The model learns through trial and error: compact too early and you lose critical details. Compact too late and context fills up with noise. Extract key facts before compacting and you get the best of both worlds. This is trainable with RLVR. </p> <h3>The Core Principle</h3> <div class="code-example"> Unlimited context &rarr; lazy agent &rarr; dump everything &rarr; hope attention finds signal (it often doesn't) Tight context &rarr; disciplined agent &rarr; precise retrieval &rarr; every token earns its place (attention concentrated on signal)</div> <div class="note-block"> <strong>Key Insight:</strong> A <strong>100K agent that's selective will outperform a 1M agent that's sloppy</strong> because: <ul> <li>Higher signal-to-noise ratio &rarr; attention works better on what's there</li> <li>Less "lost in the middle" because there's less middle</li> <li>Faster per-turn &rarr; more iterations possible &rarr; better refinement</li> <li>Cheaper per turn &rarr; can afford longer sessions</li> </ul> </div> <h2>3. At Which Stage Can You Do This?</h2> <p> LLM development has four stages. Each one offers different knobs for improving context, at very different cost levels. Understanding which stage to operate at is critical. </p> <div class="code-example"> Stage 1: Pretraining &rarr; set context capacity, learn attention patterns Stage 2: Mid-Training &rarr; extend context with RoPE scaling Stage 3: Post-Training (SFT/RL) &rarr; teach smart context management behaviors Stage 4: Agent Scaffolding &rarr; system-level context management</div> <h3>Stage 1: Pretraining</h3> <p><strong>What you can do:</strong></p> <ul> <li>Choose the positional encoding scheme (RoPE, ALiBi)</li> <li>Choose the attention architecture (full, sparse, sliding window)</li> <li>Train on progressively longer sequences (4K &rarr; 16K &rarr; 64K &rarr; 128K)</li> <li>Curate long-context training data (full codebases, books, legal documents)</li> <li>Build in GQA for KV cache efficiency</li> </ul> <p><strong>Who does this:</strong> Only frontier labs (OpenAI, Anthropic, Google, Meta, DeepSeek). This is where the fundamental context capacity is set. Everything downstream is constrained by what happens here.</p> <p><strong>What it costs:</strong> Tens of millions to billions of dollars. Training on 128K sequences costs 1,024x more per sample than 4K (quadratic attention). You need thousands of GPUs for weeks.</p> <p><strong>What you get:</strong> A model that can physically attend to 128K-200K tokens and has actually practiced using long-range attention patterns during training. This is what makes Claude, GPT-4, and Gemini good at long context. They spent enormous compute on long-context pretraining.</p> <div class="note-block"> <strong>Realistic for individuals?</strong> No. You use someone else's pretrained model. </div> <h3>Stage 2: Mid-Training (Context Extension)</h3> <p><strong>What you can do:</strong></p> <ul> <li>Take a model pretrained at 4K-8K and extend to 32K-128K</li> <li>Adjust RoPE parameters (YaRN, NTK scaling, change base frequency)</li> <li>Continue training on 10-100B tokens of long-context data</li> </ul> <p><strong>Real examples:</strong></p> <div class="code-example"> Llama 2 (4K) &rarr; Code Llama (100K) via RoPE scaling + continued training Mistral (8K) &rarr; Mistral-128K via YaRN + fine-tuning Qwen (32K) &rarr; extended versions via NTK-aware scaling</div> <p><strong>What it costs:</strong> Still expensive. You need long-context training data (books, repos, long docs) and significant GPU time. Context extension works well up to ~4x the original length. 32K &rarr; 128K is fine. 32K &rarr; 1M is sketchy.</p> <div class="quantization-table"> <table> <thead> <tr> <th>Extension</th> <th>Quality</th> <th>Estimated Cost</th> </tr> </thead> <tbody> <tr> <td>4K &rarr; 16K (4x)</td> <td>Good</td> <td>~$10K-50K (days on a small cluster)</td> </tr> <tr> <td>4K &rarr; 32K (8x)</td> <td>Good</td> <td>~$50K-200K</td> </tr> <tr> <td>4K &rarr; 128K (32x)</td> <td>Acceptable</td> <td>~$200K-1M</td> </tr> <tr> <td>4K &rarr; 1M (250x)</td> <td>Degraded</td> <td>~$1M+ (and quality is questionable)</td> </tr> </tbody> </table> </div> <div class="note-block"> <strong>Realistic for individuals?</strong> Only at the small end (3B model, 4x extension). Most people use models that were already extended by the original lab. </div> <h3>Stage 3: Post-Training (SFT and RL)</h3> <p><strong>What you can do:</strong></p> <ul> <li>Fine-tune on long-context tasks (needle-in-a-haystack, multi-doc QA)</li> <li>SFT on full multi-turn agent trajectories with tool use</li> <li>RLVR to teach the model <strong>when</strong> to compact, extract, and manage context</li> <li>Train with compaction/memory tools as available actions</li> </ul> <p> This is where you teach the model to be <strong>smart within its window</strong>. The context capacity is already set by pretraining. Post-training teaches the model behavioral strategies: </p> <div class="code-example"> What SFT teaches: "Here's what a good trajectory looks like when context gets long. See how the agent compacts at step 5? Copy that." What RLVR teaches: "Did you solve the task? No? You ran out of context because you didn't compact. Another rollout where you compacted early succeeded. Do more of that."</div> <p> RLVR is particularly powerful here because optimal context management is <strong>task-dependent</strong>. Sometimes you should compact aggressively. Sometimes you need every detail. The model learns the strategy through trial and error, not imitation. </p> <p><strong>What it costs:</strong></p> <div class="code-example"> SFT on long-context trajectories: ~$100-500 (few hours on 4x A100) RLVR for context management: ~$300-700 (part of the full RLVR budget) Total: ~$500-1200</div> <div class="note-block"> <strong>Realistic for individuals?</strong> Yes. This is the sweet spot. You take an existing model with a 32K window and teach it to be intelligent about using that window. No architecture changes, no massive compute, just better behavior. </div> <h3>Stage 4: Agent Scaffolding</h3> <p><strong>What you can do:</strong></p> <ul> <li>Automatic context compaction (summarize old turns when approaching limits)</li> <li>Prompt caching (don't reprocess the same system prompt every turn)</li> <li>Selective file reading (tools load specific functions, not entire files)</li> <li>Conversation summarization (compress old turns into key decisions)</li> <li>KV cache management across turns</li> </ul> <p> This is what Claude Code, Cursor, and Codex actually do. The model itself doesn't change. The <strong>system around the model</strong> manages context intelligently. </p> <div class="code-example"> What Claude Code does behind the scenes: 1. Prompt caching: System prompt (2K tokens) cached server-side. Not reprocessed every turn. 2. Selective reads: Agent uses Grep/Glob to find relevant files, then reads only those. Not the whole codebase. 3. Auto compaction: When context approaches 200K, older turns get summarized into ~3K token summaries. 4. KV caching: Cached prefixes avoid recomputing attention for the stable part of the conversation.</div> <p><strong>What it costs:</strong> Engineering time only. No GPU costs beyond normal inference.</p> <div class="note-block"> <strong>Realistic for individuals?</strong> Absolutely. Anyone building an agent can implement these strategies. This is the cheapest and most accessible approach. </div> <h3>Summary: The Four Stages</h3> <div class="quantization-table"> <table> <thead> <tr> <th>Stage</th> <th>What It Does</th> <th>Cost</th> <th>Who Does It</th> </tr> </thead> <tbody> <tr> <td><strong>Pretraining</strong></td> <td>Sets context capacity, trains long-range attention</td> <td>$10M-1B+</td> <td>Frontier labs only</td> </tr> <tr> <td><strong>Mid-Training</strong></td> <td>Extends context window with RoPE scaling</td> <td>$10K-1M</td> <td>Labs, well-funded startups</td> </tr> <tr> <td><strong>Post-Training (SFT/RL)</strong></td> <td>Teaches smart context management behaviors</td> <td>$500-1200</td> <td>Anyone with a few GPUs</td> </tr> <tr> <td><strong>Agent Scaffolding</strong></td> <td>System-level context management</td> <td>$0 (engineering time)</td> <td>Anyone building agents</td> </tr> </tbody> </table> </div> <h2>4. Honest Cost Analysis</h2> <p> Everyone talks about long context. Nobody talks about what it actually costs. Here's the full picture. </p> <h3>Cost of Training with Long Context</h3> <p> Attention is O(n&sup2;). This means training costs scale quadratically with context length: </p> <div class="quantization-table"> <table> <thead> <tr> <th>Context Length</th> <th>Cost per Training Sample (relative to 4K)</th> <th>What You Need</th> </tr> </thead> <tbody> <tr> <td>4K</td> <td>1x (baseline)</td> <td>Standard GPU setup</td> </tr> <tr> <td>32K</td> <td>64x</td> <td>Flash Attention, multi-GPU</td> </tr> <tr> <td>128K</td> <td>1,024x</td> <td>Multi-node, Ring Attention</td> </tr> <tr> <td>1M</td> <td>62,500x</td> <td>Frontier lab compute budget</td> </tr> </tbody> </table> </div> <h3>Cost of Inference with Long Context</h3> <p>Using Claude/GPT-4 API pricing as reference:</p> <div class="quantization-table"> <table> <thead> <tr> <th>Context Used</th> <th>Cost per Turn (approximate)</th> <th>30-Turn Session Cost</th> </tr> </thead> <tbody> <tr> <td>4K</td> <td>~$0.01</td> <td>~$0.30</td> </tr> <tr> <td>32K</td> <td>~$0.08</td> <td>~$2.40</td> </tr> <tr> <td>100K</td> <td>~$0.25</td> <td>~$7.50</td> </tr> <tr> <td>200K</td> <td>~$0.50</td> <td>~$15.00</td> </tr> <tr> <td>1M</td> <td>~$2.50</td> <td>~$75.00</td> </tr> </tbody> </table> </div> <p> At 1M context, a 30-turn coding session costs $75. Most of those tokens are noise the model barely attends to. A smart agent at 32K would cost $2.40 and probably produce better results. </p> <h3>Cost of Self-Hosted Inference</h3> <div class="code-example"> Self-hosted Qwen 3B on A100 ($1.10/hr): 4K context: ~2700 tasks/hour &rarr; $0.0004/task 32K context: ~500 tasks/hour &rarr; $0.002/task Self-hosted 70B on 4x A100 ($4.40/hr): 4K context: ~200 tasks/hour &rarr; $0.022/task 32K context: ~50 tasks/hour &rarr; $0.088/task 128K context: ~10 tasks/hour &rarr; $0.440/task</div> <h3>The Cost-Performance Sweet Spot</h3> <div class="code-example"> Approach Cost Quality Bigger model + bigger context (brute) $$$$$ Diminishing returns past 100K Same model + smarter agent behavior $$ Often better than brute force Small model + RLVR + smart context mgmt $ Best value for production</div> <div class="note-block"> <strong>The honest conclusion:</strong> Past 100K tokens, you're paying exponentially more for linearly diminishing returns. The economically rational approach is to <strong>invest in smarter context management</strong> (post-training + agent scaffolding) rather than bigger context windows. A 32K agent that compacts well beats a 200K agent that wastes context. </div> <h2>5. What Reasoning Over Long Context Actually Means</h2> <p> With the mechanics and costs established, let's address what everyone is actually chasing: making models <strong>reason across</strong> long context, not just hold it. </p> <h3>Long-Horizon Tasks: The Motivation</h3> <p> <strong>Long-horizon tasks</strong> require many sequential steps, decisions, or intermediate sub-goals before reaching a final outcome. "Reasoning over" them means the model must plan, track state, and make coherent decisions across that entire chain. </p> <div class="quantization-table"> <table> <thead> <tr> <th>Domain</th> <th>Short-Horizon</th> <th>Long-Horizon</th> </tr> </thead> <tbody> <tr> <td>Coding</td> <td>"Fix this typo"</td> <td>"Build a REST API with auth, DB, tests, and deploy it"</td> </tr> <tr> <td>Math</td> <td>"What is 2+3?"</td> <td>"Prove this theorem using 15 intermediate lemmas"</td> </tr> <tr> <td>Agents</td> <td>"Search the web"</td> <td>"Research a topic, synthesize findings, write a report, iterate on feedback"</td> </tr> </tbody> </table> </div> <h4>Why It's Hard for LLMs</h4> <ol> <li><strong>Error compounding:</strong> A small mistake in step 3 of 20 derails everything downstream.</li> <li><strong>State tracking:</strong> The model must remember what it has done, what remains, and what intermediate results it produced.</li> <li><strong>Planning under uncertainty:</strong> Early decisions constrain later options.</li> <li><strong>Credit assignment:</strong> When the final answer is wrong, it's hard to identify <em>which</em> step failed.</li> </ol> <h3>The Difference Between "Holds" and "Reasons Across"</h3> <p> This is the crux. Many models support 128K tokens. The question is what happens to information at various positions: </p> <div class="code-example"> Model that HOLDS long context: &check; Can regurgitate text from position 60K if asked "what was in file X?" &cross; Does NOT spontaneously use info from position 60K when generating code at position 120K Model that REASONS ACROSS long context: &check; While generating code at position 120K, attention heads actively pull information from position 60K because it's relevant &check; Does this without being explicitly told "look at file X" &check; Does this even when the connection is implicit (same variable name, compatible type signature, related business logic)</div> <h3>What "Reasoning Across" Looks Like in Code</h3> <h4>Cross-File Causal Tracing</h4> <pre><code class="language-python"># File A (in context at position ~2K)
def get_user(id):
    return db.query(User).filter(User.user_id == id).first()  # returns None if not found

# File B (in context at position ~15K)
def login(request):
    user = get_user(request.id)
    token = generate_token(user.email)  # ← crashes: NoneType has no attribute 'email'</code></pre> <p> The model must connect a <code>None</code> return in one file to an unguarded attribute access in another file. These two code fragments might be <strong>13,000 tokens apart</strong> in the context. The model's attention mechanism must literally assign high attention weight from the <code>user.email</code> token to the <code>return ... .first()</code> token across that gap. </p> <p> <strong>This is what "lost in the middle" kills.</strong> A weaker model sees File B, generates a fix like <code>if user is None: return error</code>, which is correct but shallow. A model reasoning across full context also notices that <code>get_user</code> should probably raise <code>UserNotFoundError</code> instead of returning <code>None</code>, because 6 other call sites (also in context from earlier grep results) all assume it returns a valid user. </p> <h4>Pattern Recognition Across the Codebase</h4> <p>After reading 8-10 files, the model should recognize:</p> <ul> <li>"This codebase uses the repository pattern"</li> <li>"Errors are custom exceptions caught by middleware, not return codes"</li> <li>"Every endpoint has a corresponding Pydantic schema in <code>schemas/</code>"</li> <li>"Tests use factory_boy fixtures, not raw object creation"</li> </ul> <p> This is <strong>not</strong> stored in any single file. It emerges from reasoning across the <strong>aggregate</strong> context. A model that can't do this generates code that is locally correct but <strong>stylistically alien</strong> to the codebase. </p> <h4>Multi-Step Plan Coherence</h4> <div class="code-example"> Step 1: Add new column to User model &rarr; produces migration Step 2: Update UserSchema to include field &rarr; must match the column type from step 1 Step 3: Update create_user service &rarr; must use the schema from step 2 Step 4: Update API endpoint &rarr; must match the service signature from step 3 Step 5: Add test &rarr; must test the endpoint from step 4 with the schema from step 2 using the model from step 1</div> <p> By step 5, the model is writing test code that must be <strong>simultaneously consistent with decisions made in steps 1-4</strong>. If the model "forgets" that it used <code>account_id</code> in step 1 and writes <code>user_id</code> in the step 5 test, <strong>the entire chain breaks</strong>. </p> <h3>Why This Is So Hard (The Non-Obvious Part)</h3> <p> The real difficulty isn't memory. It's <strong>relevance detection at scale</strong>. </p> <p> At 100K tokens of context, there might be 500 function definitions, 200 class attributes, 50 config values, and 100 test assertions. When generating one line of code, maybe 3-4 of those are relevant. The model must: </p> <ol> <li><strong>Not attend to</strong> the 796 irrelevant items (noise suppression)</li> <li><strong>Strongly attend to</strong> the 4 relevant items (signal detection)</li> <li>Do this <strong>for every token it generates</strong></li> </ol> <div class="code-example"> Generating: token = generate_token(user.?????) To decide this next token, the model must: - Recall User model definition (position 8K): field is called "email_address" not "email" - Recall generate_token signature (position 22K): first param is type str - Recall project convention (positions 5K, 12K, 31K): always uses model attributes directly - Ignore 99.5% of everything else in context &rarr; Output: "email_address"</div> <h3>How Models Address Long-Horizon Reasoning</h3> <ul> <li><strong>Chain-of-thought (CoT):</strong> Explicit step-by-step reasoning reduces the per-step difficulty.</li> <li><strong>Tree/graph search (ToT, GoT):</strong> Explore multiple reasoning paths, backtrack when stuck.</li> <li><strong>Reinforcement learning (GRPO, PPO):</strong> Train models to get reward for correct <em>final</em> answers, forcing them to learn robust multi-step strategies. This is what DeepSeek-R1 and OpenAI's o1/o3 do.</li> <li><strong>Process reward models (PRMs):</strong> Give reward at <em>each step</em>, not just the final answer, so the model learns which intermediate steps are good.</li> <li><strong>Decomposition:</strong> Break the long task into sub-tasks, solve each, then compose.</li> </ul> <div class="note-block"> <strong>Key Insight:</strong> Supervised fine-tuning alone struggles with long-horizon reasoning because it only imitates. RL lets the model <em>discover</em> better strategies through exploration. The gap between models that can chain 5 steps vs. 50 steps reliably is exactly the frontier of "reasoning models" right now. </div> <h3>How Agentic Tools Put It All Together</h3> <p> No vector database. No knowledge graph. No external memory. Just a flat conversation where the <strong>model is simultaneously the retriever, the planner, and the reasoner</strong>. </p> <div class="code-example"> Msg 1: [system] System prompt + project instructions (~3K tokens) Msg 2: [user] "Fix the auth bug" (~50 tokens) Msg 3: [assistant] Thinking + tool_call: Grep("auth error") (~200 tokens) Msg 4: [tool] Grep results: routes/auth.py:47 (~500 tokens) Msg 5: [assistant] Thinking + tool_call: Read("routes/auth.py") Msg 6: [tool] File contents (~2K tokens) Msg 7: [assistant] Thinking + tool_call: Read("services/user.py") Msg 8: [tool] File contents (~1.5K tokens) Msg 9: [assistant] Thinking + tool_call: Edit(...) Msg 10: [tool] Edit successful Msg 11: [assistant] "Fixed. Here's what I changed..."</div> <p> At message 11, the model sees <strong>ALL of messages 1-10</strong>. "Long context reasoning" = at this point, attention reaches back across every previous message simultaneously to produce a coherent fix. </p> <h4>The Model IS the Retriever</h4> <div class="code-example"> RAG retriever: "This text is 0.87 similar to the query" (statistical) Agentic retriever: "The crash is in login() calling get_user(), I need to read where get_user is defined" (causal reasoning)</div> <p> No embedding approximation. No similarity threshold. The model reasons about code dependencies and retrieves based on <strong>understanding</strong>, not pattern matching. And the same model choosing what to retrieve will use what it retrieves. </p> <h4>Iterative Context Building</h4> <div class="code-example"> Turn 1: Read error &rarr; "login endpoint crashes with NoneType" Turn 2: Read auth.py &rarr; "user.email fails because user is None" Turn 3: Grep get_user &rarr; "get_user is in services/user.py, returns .first()" Turn 4: Read user.py &rarr; ".first() returns None when no match, no error raised" Turn 5: Read tests &rarr; "tests expect UserNotFoundError, but it's never raised"</div> <p> Each turn refines understanding. By turn 5, the model has a complete causal chain in context AND has been building its mental model incrementally. This is fundamentally better than dumping 5 files cold. The <strong>order of discovery</strong> helps the model organize information. </p> <h4>Automatic Context Compression</h4> <div class="code-example"> Before (approaching limit): Messages 1-30: Full verbatim content (~180K tokens) After compression: Messages 1-15: Summarized by a fast model (~8K tokens) "Investigated auth bug. get_user() returns None instead of raising. Fixed routes/auth.py and services/user.py. User asked to update tests." Messages 16-30: Full verbatim content (~90K tokens) Total: ~98K, back under budget</div> <p> This mirrors what attention does naturally (recent = high attention, old = low attention), but makes it <strong>explicit and honest</strong> instead of pretending the model attends well to 180K tokens. </p> <h2>6. Where the Field Is Heading</h2> <div class="code-example"> Phase 1 (2023): "Make context bigger" &rarr; brute force, GPT-4 128K, Gemini 1M Phase 2 (2024): "Make context smarter" &rarr; better attention training, YaRN, RoPE scaling Phase 3 (2025+): "Make agents leaner" &rarr; use less context more effectively &boxvr;&boxh; Surgical retrieval (read functions, not files) &boxvr;&boxh; Active eviction (discard stale context proactively) &boxvr;&boxh; Structured memory (notes &gt; verbatim copies) &boxvr;&boxh; Plan-then-retrieve (think before reading) &boxur;&boxh; Compaction as a learnable skill (RLVR trains when to compress)</div> <p> The endgame is not "2M context." It's an agent that navigates a million-file codebase using a 50K context window, because it knows exactly where to look and what to remember. The next breakthrough won't be bigger windows. It'll be <strong>same quality at 64K with 10x less cost and 5x less latency</strong>. </p> <div class="note-block"> <strong>Bottom Line:</strong> Context length is determined by architecture (positional encodings, attention cost, KV cache). It can be extended at different training stages, each at vastly different costs. But the most impactful and accessible approach is to make the model smarter within its existing window, through post-training (SFT/RLVR) and agent-level strategies. A disciplined 32K agent that compacts, extracts, and plans will outperform a sloppy 200K agent on real tasks, at a fraction of the cost. </div> <h2>Key Concepts Reference</h2> <div class="quantization-table"> <table> <thead> <tr> <th>Concept</th> <th>One-Line Summary</th> </tr> </thead> <tbody> <tr> <td>RoPE (Rotary Position Embedding)</td> <td>Encodes position as rotation angle; works within training range, degrades outside it</td> </tr> <tr> <td>YaRN / NTK scaling</td> <td>Scale RoPE frequency base instead of positions; better local resolution, still needs fine-tuning</td> </tr> <tr> <td>O(n&sup2;) attention cost</td> <td>Why doubling context = 4x compute, making 1M context 25x costlier than 200K</td> </tr> <tr> <td>KV cache</td> <td>Stored Key/Value vectors per token per layer; 327 KB/token for 70B model, main memory bottleneck</td> </tr> <tr> <td>GQA (Grouped Query Attention)</td> <td>Share KV heads across query heads to reduce KV cache size (e.g., 64 query &rarr; 8 KV heads)</td> </tr> <tr> <td>Flash Attention</td> <td>Exact full attention with O(n) memory via tiled computation. No quality loss. Not sparse attention.</td> </tr> <tr> <td>Sparse Attention</td> <td>Local + global + random patterns. O(n) cost but drops some connections. Quality loss.</td> </tr> <tr> <td>Lost in the middle</td> <td>Models attend to start/end of context but lose information in the middle</td> </tr> <tr> <td>Effective vs. claimed context</td> <td>Architectural support &ne; actual retrieval/reasoning ability at that length</td> </tr> <tr> <td>Training distribution mismatch</td> <td>Model never practiced attending to position 50K if trained on 4K docs</td> </tr> <tr> <td>Context trilemma</td> <td>Long + Quality + Efficient: pick at most two</td> </tr> <tr> <td>Long-horizon reasoning</td> <td>Planning and executing across many dependent steps without error compounding</td> </tr> <tr> <td>Cross-file causal tracing</td> <td>Connecting cause in file A to effect in file B across large token distances</td> </tr> <tr> <td>Holds vs. reasons across</td> <td>Storing tokens &ne; actively using them during generation</td> </tr> <tr> <td>Relevance detection at scale</td> <td>Picking the 4 relevant items out of 800 at every generation step</td> </tr> <tr> <td>Process reward models</td> <td>Rewarding each reasoning step, not just the final answer</td> </tr> <tr> <td>Surgical retrieval</td> <td>Reading only the relevant function/lines instead of entire files</td> </tr> <tr> <td>Context eviction</td> <td>Proactively discarding stale information to keep context tight</td> </tr> <tr> <td>Structured memory</td> <td>Storing extracted facts + line pointers instead of verbatim file content</td> </tr> <tr> <td>Plan-then-retrieve</td> <td>Spending tokens thinking about what to read before reading anything</td> </tr> <tr> <td>Model-as-retriever</td> <td>The generating model also decides what to retrieve, replacing separate embedding-based search</td> </tr> <tr> <td>Automatic context compression</td> <td>Summarize old turns when approaching limits; honest version of attention decay</td> </tr> <tr> <td>Prompt caching</td> <td>Cache repeated prefixes server-side to avoid reprocessing the same system prompt every turn</td> </tr> </tbody> </table> </div>]]></content><author><name></name></author><category term="engineering"/><category term="llm"/><category term="systems"/><summary type="html"><![CDATA[What determines context in LLMs, what it costs, and which approaches work.]]></summary></entry><entry><title type="html">Serving LLMs with vLLM on RunPod: A Complete Guide</title><link href="https://shekkari1999.github.io/blog/2026/vllm-runpod-serving/" rel="alternate" type="text/html" title="Serving LLMs with vLLM on RunPod: A Complete Guide"/><published>2026-02-03T09:00:00+00:00</published><updated>2026-02-03T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2026/vllm-runpod-serving</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2026/vllm-runpod-serving/"><![CDATA[<h2>What Are We Building?</h2> <p> When you want to run an LLM for inference, you have two options: use a cloud API (OpenAI, Anthropic) or host your own. Self-hosting gives you control over costs, latency, and model choice. In this post, I'll break down exactly what happens when you deploy a model on RunPod using vLLM. </p> <div class="code-example"> The Stack: ┌─────────────────────────────────────────────────┐ │ Your Application (API calls) │ └─────────────────────┬───────────────────────────┘ │ HTTPS ▼ ┌─────────────────────────────────────────────────┐ │ RunPod Proxy (yixnlsxbw3md1q-8000.proxy...) │ └─────────────────────┬───────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Docker Container (vllm/vllm-openai:latest) │ │ ┌─────────────────────────────────────────┐ │ │ │ vLLM Server (OpenAI-compatible API) │ │ │ │ - /v1/chat/completions │ │ │ │ - /v1/completions │ │ │ │ - /v1/models │ │ │ └─────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────┐ │ │ │ Model Weights (Qwen2.5-7B-Instruct) │ │ │ │ Loaded in GPU VRAM (~14GB) │ │ │ └─────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ NVIDIA A6000 Ada GPU (48GB VRAM) │ └─────────────────────────────────────────────────┘</div> <h2>Understanding RunPod</h2> <p> RunPod is a cloud GPU provider. Unlike AWS or GCP where you rent full VMs, RunPod specializes in GPU containers. You pay only for GPU time, often at 50-70% lower cost than major cloud providers. </p> <h3>What RunPod Provides</h3> <div class="code-example"> RunPod Pod = GPU + Container Runtime + Networking Components: 1. GPU Hardware → NVIDIA A6000, A100, H100, etc. 2. Container → Docker image runs your application 3. Storage → Volume (persistent) + Container disk (ephemeral) 4. Networking → Proxy URL for HTTP/HTTPS access 5. SSH Access → Direct terminal access to the container</div> <h3>Templates: Pre-configured Recipes</h3> <p> A RunPod template is a pre-configured deployment recipe. It specifies everything needed to run a specific application: </p> <div class="code-example"> Template Components: ───────────────────────────────────────────────── Component │ Example Value ───────────────────────────────────────────────── Docker Image │ vllm/vllm-openai:latest GPU Type │ NVIDIA A6000 Ada (48GB) Volume Disk │ 40GB (model weights stored here) Container Disk │ 10GB (temporary runtime files) Environment Vars │ HF_TOKEN, HF_HOME, etc. Start Command │ python -m vllm.entrypoints... Exposed Ports │ 8000 (HTTP), 22 (SSH) ─────────────────────────────────────────────────</div> <h2>Understanding vLLM</h2> <p> vLLM is a high-performance inference engine for LLMs. It's not a model—it's the software that loads models and serves them efficiently. </p> <h3>Why vLLM Over Plain PyTorch?</h3> <div class="code-example"> Plain PyTorch Inference: - Load model into GPU - Process one request at a time - Recompute KV cache for each token - Result: ~20 tokens/sec vLLM Inference: - PagedAttention: Efficient memory management - Continuous Batching: Process multiple requests simultaneously - KV Cache Optimization: Reuse computed attention states - Result: ~50-100+ tokens/sec</div> <h3>Key vLLM Optimizations</h3> <p><strong>1. PagedAttention</strong></p> <p> Traditional attention stores KV cache in contiguous memory blocks. vLLM uses "paged" memory (like OS virtual memory), allowing dynamic allocation and preventing memory fragmentation. </p> <p><strong>2. Continuous Batching</strong></p> <p> Instead of waiting for a batch to complete, vLLM continuously adds new requests and removes completed ones. This maximizes GPU utilization. </p> <div class="code-example"> Traditional Batching: Request 1: [████████████████████] Request 2: [████████████████████] Request 3: [████████████████████] ↑ Wait for all to finish before next batch Continuous Batching: Request 1: [████████] Request 2: [████████████████] Request 3: [████████████] Request 4: [████████████████] ↑ New requests added as slots free up</div> <h2>The Deployment Flow</h2> <p>Here's exactly what happens when you deploy:</p> <h3>Step 1: Pod Creation</h3> <div class="code-example"> RunPod allocates: - 1x NVIDIA A6000 Ada GPU (48GB VRAM) - 32GB System RAM - 40GB Volume Disk (mounted at /workspace) - 10GB Container Disk Time: ~30 seconds</div> <h3>Step 2: Container Startup</h3> <div class="code-example"> Docker pulls: vllm/vllm-openai:latest Container contains: - Python 3.10+ - PyTorch with CUDA support - vLLM library - Transformers library - FastAPI server Time: ~1-2 minutes (if image not cached)</div> <h3>Step 3: Model Download</h3> <div class="code-example"> vLLM downloads from HuggingFace: Model: Qwen/Qwen2.5-7B-Instruct Files: - model.safetensors.index.json - model-00001-of-00004.safetensors (4GB each) - tokenizer.json - config.json Total Size: ~14GB Saved to: /workspace/hf_home/hub/models--Qwen--Qwen2.5-7B-Instruct/ Time: 5-10 minutes (first time)</div> <h3>Step 4: Model Loading</h3> <div class="code-example"> vLLM loads model into GPU: 1. Read safetensors files from disk 2. Convert to appropriate dtype (bfloat16/float16) 3. Transfer weights to GPU VRAM 4. Initialize KV cache blocks 5. Compile CUDA graphs (optional) Memory Layout: ┌─────────────────────────────────────────┐ │ A6000 GPU (48GB VRAM) │ ├─────────────────────────────────────────┤ │ Model Weights │ ~14GB │ │ KV Cache │ ~20GB (dynamic) │ │ Activations │ ~4GB │ │ Free │ ~10GB │ └─────────────────────────────────────────┘ Time: 1-2 minutes</div> <h3>Step 5: API Server Ready</h3> <div class="code-example"> vLLM starts FastAPI server: INFO: Uvicorn running on http://0.0.0.0:8000 Available Endpoints: ───────────────────────────────────────────────── Endpoint │ Method │ Purpose ───────────────────────────────────────────────── /v1/models │ GET │ List loaded models /v1/chat/completions │ POST │ Chat API (OpenAI format) /v1/completions │ POST │ Text completion /health │ GET │ Health check ───────────────────────────────────────────────── RunPod creates proxy: https://yixnlsxbw3md1q-8000.proxy.runpod.net → localhost:8000</div> <h2>Making Requests</h2> <p> vLLM exposes an OpenAI-compatible API. This means you can use the same code you'd use for OpenAI, just change the base URL: </p> <div class="code-example"> # OpenAI API call curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer \*\*\*" \ -d '{"model": "gpt-4", "messages": [...]}' # vLLM API call (same format!) curl https://your-pod-8000.proxy.runpod.net/v1/chat/completions \ -H "Authorization: Bearer \*\*\*" \ -d '{"model": "Qwen/Qwen2.5-7B-Instruct", "messages": [...]}'</div> <h3>Request Flow</h3> <div class="code-example"> 1. Request arrives at vLLM server 2. Tokenizer converts text → token IDs "What is ML?" → [1724, 374, 14946, 30] 3. Tokens added to scheduling queue 4. Scheduler batches with other requests 5. Forward pass through model: - Embedding lookup - 28 transformer layers - Final linear → logits 6. Sampling (temperature, top_p) 7. New token generated 8. Repeat until stop condition 9. Detokenize → text response 10. Return JSON with usage stats</div> <h2>Benchmark Results</h2> <p> I ran benchmarks at different concurrency levels to understand throughput scaling: </p> <div class="code-example"> Configuration: - Model: Qwen/Qwen2.5-VL-7B-Instruct - GPU: NVIDIA A6000 Ada (48GB) - Cost: $0.44/hour - Max tokens per request: 100 Results: ───────────────────────────────────────────────────────────────── Concurrency │ Avg Latency │ P50 Latency │ P95 Latency │ Tokens/s ───────────────────────────────────────────────────────────────── 1 │ 2068ms │ 2018ms │ 2276ms │ 48.49 4 │ 2060ms │ 2070ms │ 2251ms │ 48.68 8 │ 2156ms │ 2145ms │ 2376ms │ 46.55 ───────────────────────────────────────────────────────────────── Throughput Scaling: - 1 concurrent: 0.48 req/s - 4 concurrent: 1.91 req/s (4x improvement) - 8 concurrent: 3.64 req/s (7.6x improvement)</div> <div class="note-block"> <strong>Key Observation:</strong> Latency stays relatively flat even as concurrency increases. This is continuous batching in action—vLLM efficiently processes multiple requests without proportionally increasing per-request latency. </div> <h2>Cost Analysis</h2> <p> Self-hosting only makes sense if it's cheaper than API providers. Let's do the math: </p> <div class="code-example"> Calculating Cost Per 1M Output Tokens: Given: - GPU cost: $0.44/hour - Average tokens/sec: 47.91 - Tokens per hour: 47.91 × 3600 = 172,476 Cost per token = $0.44 / 172,476 = $0.00000255 Cost per 1M tokens = $2.55 Comparison: ───────────────────────────────────────────────── Provider │ Cost/1M tokens │ vs Self-Host ───────────────────────────────────────────────── GPT-4o │ $15.00 │ 5.9x more expensive GPT-4o-mini │ $0.60 │ 4.2x cheaper Claude 3.5 Sonnet │ $15.00 │ 5.9x more expensive Self-hosted vLLM │ $2.55 │ baseline ─────────────────────────────────────────────────</div> <div class="note-block"> <strong>Trade-offs:</strong> <ul> <li class="benefit">Self-hosting beats frontier models (GPT-4o, Claude) on cost</li> <li class="drawback">GPT-4o-mini is still cheaper for simple tasks</li> <li class="benefit">Self-hosting: no rate limits, full control, privacy</li> <li class="drawback">Self-hosting: you manage infrastructure, no automatic scaling</li> </ul> </div> <h2>When to Self-Host vs Use APIs</h2> <div class="code-example"> Self-Host When: ✓ High volume (>1M tokens/day) ✓ Need low latency (<500ms) ✓ Privacy requirements (data can't leave your infra) ✓ Need fine-tuned/custom models ✓ Predictable, steady traffic Use APIs When: ✓ Low/variable volume ✓ Need frontier model quality (GPT-4, Claude) ✓ Don't want to manage infrastructure ✓ Need automatic scaling ✓ Experimenting/prototyping</div> <h2>Key Takeaways</h2> <div class="note-block"> <strong>What We Learned:</strong> <ul> <li>RunPod provides GPU containers with simple deployment via templates</li> <li>vLLM is an inference engine that makes LLM serving 2-5x faster than naive PyTorch</li> <li>PagedAttention and continuous batching are the key optimizations</li> <li>Self-hosting a 7B model costs ~$2.55/1M tokens on A6000</li> <li>Throughput scales well with concurrency (7.6x at 8 concurrent)</li> <li>OpenAI-compatible API means easy integration with existing code</li> </ul> </div> <h3>What's Next?</h3> <p> In the next post, I'll test function calling capabilities with this setup. We'll compare zero-shot function calling accuracy across different open-source models and see how they stack up against GPT-4. </p>]]></content><author><name></name></author><category term="engineering"/><category term="inference"/><category term="systems"/><summary type="html"><![CDATA[A practical guide to serving LLMs with vLLM on RunPod.]]></summary></entry><entry><title type="html">GPU Fundamentals &amp;amp; LLM Inference Mental Models</title><link href="https://shekkari1999.github.io/blog/2026/gpu-fundamentals/" rel="alternate" type="text/html" title="GPU Fundamentals &amp;amp; LLM Inference Mental Models"/><published>2026-02-01T09:00:00+00:00</published><updated>2026-02-01T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2026/gpu-fundamentals</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2026/gpu-fundamentals/"><![CDATA[<p> Research Engineer interviews at leading AI labs test your ability to reason about inference performance from first principles. You will be expected to estimate whether a model fits on a given GPU, explain why autoregressive decoding is slow, identify bottlenecks (memory bandwidth vs. compute) for a given workload, and propose optimizations that target the actual bottleneck. </p> <p> This article builds that foundation. We cover GPU architecture, the roofline model, memory estimation, arithmetic intensity analysis, and latency estimation , everything you need to develop strong intuition about what makes LLM inference fast or slow. </p> <h2>GPU Architecture Fundamentals</h2> <h3>NVIDIA A100 Architecture</h3> <p> The A100 is the workhorse GPU for LLM inference and training. Understanding its architecture is essential for reasoning about performance. </p> <p class="key-formula"> The A100-80GB packs <strong>108 SMs</strong>, each with 64 CUDA cores and 4 Tensor cores, connected via <strong>40 MB L2</strong> to <strong>80 GB HBM2e</strong> at 2 TB/s. Fast on-chip SRAM (192 KB per SM) sits next to each SM; the size and speed gap between SRAM and HBM is what makes memory bandwidth the bottleneck. </p> <h3>Key Numbers to Memorize</h3> <div class="spec-cards"> <div class="spec-card"><span class="term">Streaming Multiprocessors</span><div class="value">108</div><div class="note">Each SM is an independent processor</div></div> <div class="spec-card"><span class="term">CUDA Cores</span><div class="value">6912 (64/SM)</div><div class="note">Scalar FP ops</div></div> <div class="spec-card"><span class="term">Tensor Cores</span><div class="value">432 (4/SM)</div><div class="note">Matrix multiply (FP16/BF16/INT8)</div></div> <div class="spec-card"><span class="term">HBM2e Capacity</span><div class="value">80 GB</div><div class="note">Main VRAM</div></div> <div class="spec-card"><span class="term">HBM Bandwidth</span><div class="value">2.0 TB/s</div><div class="note">Data rate from HBM</div></div> <div class="spec-card"><span class="term">L2 Cache</span><div class="value">40 MB</div><div class="note">Shared across SMs</div></div> <div class="spec-card"><span class="term">L1/Shared per SM</span><div class="value">192 KB</div><div class="note">Fast on-chip</div></div> <div class="spec-card"><span class="term">Total SRAM</span><div class="value">~20.25 MB</div><div class="note">108 &times; 192 KB</div></div> <div class="spec-card"><span class="term">FP16 Throughput</span><div class="value">312 TFLOPS</div><div class="note">Peak, Tensor Cores</div></div> <div class="spec-card"><span class="term">FP32 Throughput</span><div class="value">156 TFLOPS</div><div class="note">Peak FP32</div></div> </div> <h3>Execution Model: Warps and Thread Blocks</h3> <ul> <li><strong>Kernel launch</strong> &rarr; a grid of thread blocks is scheduled onto SMs.</li> <li><strong>Each block</strong> has up to 1024 threads, split into warps.</li> <li><strong>Each warp</strong> = 32 threads executing in lockstep (SIMT); they all run the same instruction at once.</li> </ul> <p> The <strong>warp</strong> is the fundamental unit of execution. All 32 threads in a warp execute the same instruction at the same time. If threads diverge (different <code>if</code> branches), both paths must be executed serially , this is called <strong>warp divergence</strong> and it wastes cycles. </p> <h3>SRAM vs. HBM: The Crucial Ratio</h3> <ul> <li><strong>Total SRAM</strong>: ~20 MB (fast, on-chip, ~30 cycle latency)</li> <li><strong>Total HBM</strong>: 80 GB (slow, off-chip, ~400 cycle latency)</li> <li><strong>Ratio</strong>: HBM is ~4000x larger but ~13x slower</li> </ul> <p> This mismatch is the fundamental reason why <strong>memory bandwidth is the bottleneck</strong> for most LLM inference workloads. The model weights live in HBM, and for each token generated during decode, we must read ALL weights from HBM through a limited bandwidth pipe. </p> <h3>Memory Hierarchy Latency</h3> <div class="memory-stack"> <div class="memory-tier"><span class="name">Registers</span><span class="latency">~1 cycle</span><span class="size">64K 32-bit regs/SM</span></div> <div class="memory-tier"><span class="name">L1 / SRAM</span><span class="latency">~30 cycles</span><span class="size">192 KB/SM = 20 MB total, ~19 TB/s aggregate</span></div> <div class="memory-tier"><span class="name">L2 Cache</span><span class="latency">~200 cycles</span><span class="size">40 MB, ~5–6 TB/s</span></div> <div class="memory-tier"><span class="name">HBM</span><span class="latency">~400 cycles</span><span class="size">80 GB, 2.0 TB/s</span></div> </div> <h3>Why This Matters for LLM Inference</h3> <p> During <strong>autoregressive decode</strong>, each generated token requires reading the full model weights from HBM: </p> <ul> <li><strong>Llama-7B at FP16</strong>: 14 GB of weights</li> <li><strong>At 2 TB/s bandwidth</strong>: Takes 14 GB / 2 TB/s = <strong>7 ms</strong> just to read the weights</li> <li><strong>Computation</strong>: ~14 GFLOP per token, which at 312 TFLOPS takes only <strong>0.045 ms</strong></li> </ul> <p> The weight-loading time is <strong>150x larger</strong> than the compute time. This is why decode is memory-bandwidth-bound, and why all the major inference optimizations (quantization, KV caching, speculative decoding, batching) ultimately aim to reduce the bytes-per-useful-FLOP ratio. </p> <div class="note-block"> <strong>Rule of thumb:</strong> If you can do the arithmetic faster than you can load the data, you are <strong>memory-bound</strong>. For single-token decode, this is almost always the case. </div> <h2>The Roofline Model</h2> <p> The <strong>roofline model</strong> is the single most important mental model for understanding inference performance. It tells you whether a workload is limited by: </p> <ol> <li><strong>Memory bandwidth</strong> (loading data from HBM) , left side of the plot</li> <li><strong>Compute throughput</strong> (doing arithmetic) , right side / top of the plot</li> </ol> <h3>Arithmetic Intensity</h3> <div class="key-formula"> <strong>AI = FLOPs / Bytes Accessed</strong> (FLOPs per byte). Low AI (&lt; ridge point) &rarr; memory-bound; high AI (&gt; ridge point) &rarr; compute-bound. </div> <h3>Ridge Point</h3> <p> The <strong>ridge point</strong> is the arithmetic intensity at which the compute and memory roofs intersect: </p> <div class="key-formula"> Ridge = Peak Compute / Peak Bandwidth. A100-80GB: 312 TFLOP/s ÷ 2.0 TB/s = <span class="result">156 FLOPs/byte</span>. </div> <h3>Attainable Performance</h3> <div class="key-formula"> Attainable = min(Peak Compute, AI &times; Bandwidth). If AI &lt; 156: memory-limited (AI &times; 2 TB/s). If AI &ge; 156: compute-limited (312 TFLOPS). </div> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/roofline.png" alt="A100-80GB Roofline Model showing Prefill vs Decode positions" class="diagram"> <div class="diagram-caption">Figure 1: The A100-80GB Roofline Model. Decode (batch=1) sits at AI=1, achieving only 0.6% of peak compute. Prefill (seq=2048) sits near the ridge point, approaching full compute utilization. The red region is memory-bound; the green region is compute-bound.</div> </div> <h3>The Key Insight: Prefill is Compute-Bound, Decode is Memory-Bound</h3> <p>This single insight explains nearly every optimization in modern LLM serving.</p> <div class="phase-cards"> <div class="phase-card prefill"> <h4>Prefill</h4> <div class="what">Process entire prompt in one shot. Matrix-matrix multiply: (seq_len &times; d_model) @ (d_model &times; d_model).</div> <div class="ai">AI scales with seq_len; for seq=2048, AI ~ 150.</div> <div class="bottleneck">Compute-bound: limited by TFLOPS</div> </div> <div class="phase-card decode"> <h4>Decode</h4> <div class="what">Generate one token at a time. Matrix-vector multiply: (1 &times; d_model) @ (d_model &times; d_model).</div> <div class="ai">AI ~ 1 (each weight byte used once per batch item).</div> <div class="bottleneck">Memory-bound: limited by HBM bandwidth</div> </div> </div> <p> <strong>Why is decode so memory-inefficient?</strong> During decode, we generate ONE token. This means we do a matrix-vector product: the weight matrix has <code>d_model &times; d_model</code> elements, but the input vector has only <code>d_model</code> elements. Each weight is loaded from HBM but used for only <strong>one multiply-add</strong> (2 FLOPs / 2 bytes at FP16 = AI of 1). </p> <p> <strong>Why is prefill efficient?</strong> During prefill, the input is a matrix of shape <code>(seq_len &times; d_model)</code>. The same weight matrix (loaded once from HBM) is multiplied against <code>seq_len</code> vectors. The weight bytes are <strong>amortized</strong> across <code>seq_len</code> tokens, giving AI ~ <code>seq_len</code>. </p> <h3>Optimization Implications</h3> <div class="opt-cards"> <div class="opt-card"><strong>Quantization</strong> (INT4/INT8) <span class="target">Decode</span>, fewer bytes from HBM per weight</div> <div class="opt-card"><strong>Batching</strong> <span class="target">Decode</span>, amortize weight load across B sequences (AI &times; B)</div> <div class="opt-card"><strong>FlashAttention</strong> <span class="target">Prefill</span>, tile attention into SRAM, fewer HBM reads</div> <div class="opt-card"><strong>Speculative Decoding</strong> <span class="target">Decode</span>, verify multiple tokens in one pass</div> <div class="opt-card"><strong>KV Cache</strong> <span class="target">Both</span>, avoid recomputing attention for past tokens</div> <div class="opt-card"><strong>Tensor Parallelism</strong> <span class="target">Both</span>, split weights across GPUs</div> </div> <h2>Memory Estimation</h2> <p> Understanding where GPU memory goes during inference is critical for capacity planning. Let's break down the memory components for Llama-7B. </p> <h3>VRAM Breakdown: Llama-7B at FP16</h3> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/vram_breakdown.png" alt="Pie chart showing VRAM breakdown for Llama-7B at FP16" class="diagram"> <div class="diagram-caption">Figure 2: Llama-7B memory breakdown (FP16, batch=1, seq=2048). Weights 87.8% (13.04 GB), KV cache 6.7% (1.00 GB), activations 3.4% (0.51 GB), CUDA overhead 2.1% (0.31 GB). Total ~14.86 GB.</div> </div> <div class="note-block"> <strong>Key observations:</strong> <ul> <li>At batch=1, model weights dominate total memory (~90%+)</li> <li>KV cache is small at batch=1 but grows linearly with batch size and sequence length</li> <li>Activations during inference are negligible (only one layer active at a time)</li> <li>CUDA overhead is a fixed ~0.5 GB cost</li> </ul> </div> <h3>Does Llama-70B Fit on A100-80GB?</h3> <div class="opt-cards"> <div class="opt-card"><strong>FP16</strong> 130 GB total &rarr; <span class="target">No</span> (exceeds 80 GB; need 2 GPUs / tensor parallelism)</div> <div class="opt-card"><strong>INT8</strong> 66.9 GB total &rarr; <span class="target">Yes</span> (~13 GB headroom)</div> <div class="opt-card"><strong>INT4</strong> 34.4 GB total &rarr; <span class="target">Yes</span> (~46 GB headroom)</div> </div> <div class="note-block"> <strong>Interview insight:</strong> Llama-70B at FP16 needs ~130 GB = 2&times; A100-80GB. Quantizing to INT4 brings it down to ~35 GB, fitting on a single GPU. This is why quantization is so important for deployment. </div> <h3>When Does KV Cache Dominate?</h3> <p> At batch=1, weights dominate. But at production batch sizes, KV cache quickly overtakes weights. This plot shows the crossover point for Llama-7B. </p> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/kv_cache_crossover.png" alt="KV Cache vs Weights memory as batch size increases" class="diagram"> <div class="diagram-caption">Figure 3: Llama-7B FP16 (seq_len=2048), KV cache vs weights vs batch size. Weights constant at ~13 GB; KV cache grows linearly. KV cache equals weights at batch=14. Max batch=50 on A100-80GB before OOM. KV cache management (PagedAttention, GQA) is critical at scale.</div> </div> <h3>Memory Across Model Sizes and Precisions</h3> <div class="model-cards"> <div class="model-card"><span class="name">Llama-7B</span><span class="dtype">FP16</span><div class="mem">Weights 13.0 GB · KV 0.50 GB · Total 14.0 GB</div><div class="fits yes">✓ Fits A100</div></div> <div class="model-card"><span class="name">Llama-7B</span><span class="dtype">INT4</span><div class="mem">Weights 3.3 GB · KV 0.50 GB · Total 4.3 GB</div><div class="fits yes">✓ Fits A100</div></div> <div class="model-card"><span class="name">Llama-13B</span><span class="dtype">FP16</span><div class="mem">Weights 24.2 GB · KV 0.78 GB · Total 25.5 GB</div><div class="fits yes">✓ Fits A100</div></div> <div class="model-card"><span class="name">Llama-13B</span><span class="dtype">INT4</span><div class="mem">Weights 6.1 GB · KV 0.78 GB · Total 7.4 GB</div><div class="fits yes">✓ Fits A100</div></div> <div class="model-card"><span class="name">Llama-70B</span><span class="dtype">FP16</span><div class="mem">Weights 130 GB · KV 1.25 GB · Total 131.9 GB</div><div class="fits no">✗ 2 GPUs</div></div> <div class="model-card"><span class="name">Llama-70B</span><span class="dtype">INT4</span><div class="mem">Weights 32.5 GB · KV 1.25 GB · Total 34.4 GB</div><div class="fits yes">✓ Fits A100</div></div> </div> <p> <strong>Key observations:</strong> </p> <ol> <li>Weights scale linearly with parameter count and inversely with quantization.</li> <li>INT4 gives a 4x reduction in weight memory vs FP16, making 70B feasible on one GPU.</li> <li>KV cache for 70B is smaller than expected thanks to GQA (8 KV heads vs 64 query heads).</li> <li>At batch=1, weights dominate. At production batch sizes (32+), KV cache dominates.</li> </ol> <h2>Arithmetic Intensity Analysis</h2> <p> Let's see how arithmetic intensity changes with different operating conditions, and how that determines whether we're memory-bound or compute-bound. </p> <h3>Decode AI vs Batch Size</h3> <p> Decode at batch=1 has an arithmetic intensity of just 1 , only 0.6% of the ridge point. Even at batch=64, we're still deeply memory-bound. It takes a batch size of ~156 to fully saturate the A100's compute capability during decode. </p> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/Decode_AI_vs_batch_size.png" alt="Decode arithmetic intensity vs batch size" class="diagram"> <div class="diagram-caption">Figure 4: Decode arithmetic intensity grows linearly with batch size. The blue dashed line marks the A100 ridge point at 156 FLOPs/byte. Below this line, the GPU's compute cores are underutilized , adding more sequences to the batch is essentially "free" in terms of latency, because we're bottlenecked on memory bandwidth anyway.</div> </div> <h3>Prefill AI vs Sequence Length</h3> <p> Prefill crosses the ridge point at relatively short sequence lengths (~300-400 tokens). For typical prompt lengths (1K+ tokens), prefill is solidly compute-bound , the GPU cores become the bottleneck, not memory bandwidth. </p> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/prefil_vs_seq.png" alt="Prefill arithmetic intensity vs sequence length" class="diagram"> <div class="diagram-caption">Figure 5: Prefill arithmetic intensity vs sequence length (Llama-7B, FP16). Crosses A100 ridge (156 FLOPs/byte) at seq_len ~192; below that, prefill is memory-bound; above, compute-bound. For short prompts, latency is dominated by weight loading; for long prompts, by computation.</div> </div> <h2>Time Estimates</h2> <p> Using the roofline model, we can estimate latencies for prefill (TTFT = time to first token) and decode (per-token generation time). These back-of-the-envelope calculations are exactly the kind of reasoning expected in interviews. </p> <h3>Prefill Latency (TTFT)</h3> <p> Prefill latency scales roughly linearly with prompt length. For Llama-7B on A100, a 2048-token prompt takes about 9 ms , fast enough to be imperceptible. </p> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/ttft_vs_prompt_length.png" alt="Time to first token vs prompt length for different model sizes" class="diagram"> <div class="diagram-caption">Figure 6: Prefill latency (TTFT) vs prompt length for Llama-7B, 13B, and 70B on A100 (FP16, batch=1). Horizontal reference lines mark 100ms (imperceptible), 500ms (noticeable), and 1000ms (slow). Larger models have proportionally higher TTFT. For Llama-70B, prompts beyond 4K tokens push TTFT past 1 second on a single A100.</div> </div> <h3>Decode Latency and Throughput</h3> <p class="key-formula"> Decode time stays ~7 ms per step while memory-bound (batch &lt; ~164); total tokens/sec scales with batch. Past the ridge, latency rises and throughput plateaus (~22K tokens/sec). See the diagram below. </p> <p> The key insight: <strong>decode time per step stays nearly constant as batch size increases</strong> (while memory-bound). This means total throughput scales linearly with batch size , for free! This is the fundamental insight behind continuous batching. </p> <div class="diagram-container"> <img src="/assets/img/blog/gpu-fundamentals/decode_latency_throughput.png" alt="Decode latency and throughput vs batch size" class="diagram"> <div class="diagram-caption">Figure 7: Left: Llama-7B decode latency per step stays flat (~7 ms) while memory-bound, then rises after the inflection at batch=164 (transition to compute-bound). Right: Throughput scales linearly with batch size in the memory-bound regime (&quot;free&quot; batching), then plateaus at ~22K tokens/sec. This is why production serving uses continuous batching.</div> </div> <div class="note-block"> <strong>Key observations:</strong> <ul> <li>Decode time per step stays nearly constant as batch size increases (while memory-bound)</li> <li>Total throughput (tokens/sec) scales linearly with batch size , for free</li> <li>This is the fundamental insight behind continuous batching (vLLM, TGI)</li> <li>Per-request latency stays the same , everyone gets the same speed, but the server handles more requests</li> </ul> </div> <h2>Key Takeaways</h2> <h3>5 Things You Should Know</h3> <ol> <li> <strong>"Autoregressive decode is memory-bandwidth-bound, not compute-bound."</strong> Each decode step requires reading all model weights from HBM to generate a single token. The arithmetic intensity is ~1 FLOP/byte (at batch=1, FP16), far below the A100's ridge point of 156. This means the Tensor Cores sit idle ~99% of the time during decode. </li> <li> <strong>"Prefill is compute-bound for reasonable sequence lengths."</strong> During prefill, the same weights are reused across all tokens in the prompt, giving arithmetic intensity that scales with <code>seq_len</code>. For seq_len > ~300 on A100, prefill becomes compute-bound. </li> <li> <strong>"Batching is the primary lever for decode throughput."</strong> Since decode is memory-bound, adding more sequences to a batch reuses the same weight data already being streamed from HBM. The per-step latency barely changes, but throughput scales linearly. This is why continuous batching (vLLM, TGI) is transformative. </li> <li> <strong>"KV cache memory grows as O(batch_size &times; seq_len &times; num_layers &times; d_head &times; num_kv_heads)."</strong> At production batch sizes (32+), KV cache can dominate total GPU memory. This is why PagedAttention, GQA (grouped-query attention), and KV cache quantization are critical. </li> <li> <strong>"Quantization helps decode more than prefill."</strong> Since decode is memory-bound, reducing the number of bytes per weight (FP16 &rarr; INT4 = 4x fewer bytes) directly translates to ~4x faster decode. For prefill (compute-bound), quantization helps less unless it also reduces the compute cost. </li> </ol> <h2>Quick-Reference Formulas</h2> <p>Keep these formulas in your head for back-of-the-envelope calculations:</p> <div class="spec-cards"> <div class="spec-card"><span class="term">Weight Memory</span><div class="value">params &times; bytes/element</div><div class="note">Llama-7B FP16: 14 GB · 70B INT4: 35 GB</div></div> <div class="spec-card"><span class="term">KV Cache</span><div class="value">batch &times; seq &times; layers &times; (2 &times; kv_heads &times; head_dim &times; bytes)</div><div class="note">Per token per layer, then sum over layers</div></div> <div class="spec-card"><span class="term">Decode AI</span><div class="value">2 &times; B / bytes_per_element</div><div class="note">B=1 &rarr; 1; B=32 &rarr; 32. Prefill AI ~ seq_len.</div></div> <div class="spec-card"><span class="term">Decode time</span><div class="value">Weight Memory / HBM BW</div><div class="note">Llama-7B FP16 on A100: 14 GB / 2 TB/s = <span class="result">7 ms</span></div></div> <div class="spec-card"><span class="term">Ridge</span><div class="value">Peak FLOPS / Peak BW</div><div class="note">A100: 156 · H100: 296 FLOPs/byte</div></div> </div>]]></content><author><name></name></author><category term="engineering"/><category term="inference"/><category term="systems"/><summary type="html"><![CDATA[GPU architecture, roofline analysis, memory estimation, and LLM inference mental models.]]></summary></entry><entry><title type="html">ML Training Optimization: FLOPs, Profiling, and Learning Strategies</title><link href="https://shekkari1999.github.io/blog/2025/ml-training-optimization/" rel="alternate" type="text/html" title="ML Training Optimization: FLOPs, Profiling, and Learning Strategies"/><published>2025-10-20T09:00:00+00:00</published><updated>2025-10-20T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/ml-training-optimization</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/ml-training-optimization/"><![CDATA[<div class="disclaimer"> <p><em>💻 Fun disclaimer: Used GPT to get all the beautiful visual gradients, but the content is mine!</em></p> </div> <p> When training large-scale machine learning models, optimization goes beyond just hyperparameter tuning. This guide covers the essential aspects of efficient ML training: computational constraints, performance profiling, and learning strategies that can save you significant costs and time. </p> <h2>1. FLOPs and Chinchilla Scaling Law</h2> <p> When training large-scale ML models, you typically have <strong>FLOPs (Floating Point Operations)</strong> constraints. The <strong>Chinchilla scaling law</strong> provides crucial guidance on how to allocate your compute budget effectively. </p> <div class="insight-card"> <div class="insight-content"> <h4>Chinchilla Scaling Law</h4> <p>For a fixed compute budget (FLOPs), you need to decide between having more parameters (bigger model) or training the model for longer (showing it more data).</p> </div> </div> <h3>Two Critical Cases to Avoid</h3> <h4>1. Compute Inefficient Training</h4> <div class="code-example"> Example: Compute Inefficiency Scenario: • You've built a model that can handle 20 data points effectively • But you only show it 10 data points • You keep training beyond what's necessary Problem: • You're wasting the model's potential • Training longer won't help if you're not using enough data • This is compute inefficient because you're not utilizing your model's capacity</div> <h4>2. Data Inefficient Training</h4> <div class="code-example"> Example: Data Inefficiency Scenario: • You've built a model that can handle 10 data points • But you're showing it 20 data points • The model can't effectively process all the information Problem: • You're wasting valuable data • The model can't learn from the excess information • This is data inefficient because you're not utilizing your data effectively</div> <div class="warning-banner"> <div class="warning-content"> <strong>Key Takeaway:</strong> Always match your model capacity with your data size and training duration to avoid both compute and data inefficiency. </div> </div> <h2>2. Profiling Your Code</h2> <p> Profiling your training code is essential for maximizing GPU utilization and getting the best performance for your investment. This is different from hyperparameter tuning, which focuses on model learning rather than computational efficiency. </p> <h3>Key Bottlenecks to Monitor</h3> <h4>I/O Bottleneck</h4> <p> Don't assume that just because your GPU can handle a larger batch size, you should use it. PyTorch data loaders work on CPU threads, and if your GPU finishes processing batch 1 but your data loader isn't ready with batch 2, your GPU sits idle. </p> <div class="code-example"> I/O Bottleneck Example: GPU Timeline: Batch 1: [████████████] Processing Batch 2: [ ] Waiting for data... Batch 3: [ ] Still waiting... CPU DataLoader: Batch 1: [████████████] Loading Batch 2: [████████████] Loading (slow) Batch 3: [ ] Not ready yet Result: GPU utilization drops significantly</div> <h4>Memory Bottleneck</h4> <p> Good profiling reveals what's consuming your memory. Common culprits include: </p> <ul> <li>Per-layer activations</li> <li>Gradients storage</li> <li>Temporary tensor assignments</li> <li>Optimizer states</li> </ul> <h5>Memory Optimization Techniques</h5> <div class="comparison-table"> <div class="comparison-column"> <h5>Gradient Checkpointing</h5> <ul> <li>Trades computation for memory</li> <li>Recomputes activations during backward pass</li> <li>Can reduce memory by 50-80%</li> </ul> </div> <div class="comparison-column"> <h5>Mixed Precision</h5> <ul> <li>Uses FP16 for forward pass</li> <li>Maintains FP32 for gradients</li> <li>Reduces memory by ~50%</li> </ul> </div> </div> <h4>CPU ↔ GPU Transfer Bottleneck</h4> <p> Moving data between CPU and GPU is often a major bottleneck due to bandwidth limitations. Common scenarios that cause this issue: </p> <ul> <li>Using <code>.item()</code> to extract scalar values</li> <li>Checkpointing weights to CPU</li> <li>Frequent data transfers during training</li> </ul> <div class="code-example"> Avoid These CPU-GPU Transfers: ❌ Bad: loss_value = loss.item() # Moves to CPU if loss_value < threshold: # Do something ✅ Good: if loss < threshold: # Keep on GPU # Do something</div> <h4>Kernel Overhead</h4> <p> Launching many small kernels can create overhead. The CPU tells the GPU to launch numerous kernels, and the GPU may struggle to keep up with the launch rate. </p> <div class="tip-card"> <div class="tip-content"> <h4>Profiling Priority</h4> <p>Always profile your code first to identify bottlenecks before focusing on accuracy improvements. This approach will save you significant costs.</p> </div> </div> <h2>3. Learning Strategies</h2> <p> Once you've optimized your computational efficiency, focus on improving model performance through effective learning strategies. </p> <h3>Batch Size Selection</h3> <p> Choose the highest batch size your GPU and data loader can handle, but ensure you maintain some stochasticity in your updates. When you change batch size, adjust your learning rate accordingly (usually linearly). </p> <div class="code-example"> Batch Size Guidelines: • MNIST Example: Don't use the entire dataset as one batch - Too smooth learning leads to local minima - Always maintain some randomness in updates • Learning Rate Adjustment: - If you double batch size, consider doubling learning rate - Monitor training dynamics carefully</div> <h3>Gradient Accumulation</h3> <p> If your learning is too noisy (loss oscillates up and down), consider gradient accumulation to smooth the updates: </p> <div class="code-example"> Gradient Accumulation Example: # Instead of: loss = model(batch) / batch_size loss.backward() optimizer.step() # Use: for i in range(accumulation_steps): loss = model(batch[i]) / batch_size loss.backward() # Accumulate gradients optimizer.step() # Update once with accumulated gradients</div> <h2>Frequently Asked Questions</h2> <h3>When do you stop training? What is the ideal loss?</h3> <div class="code-example"> Stopping Criteria: Keep training while: ✓ Validation loss decreases alongside training loss ✓ You have budget remaining ✓ No signs of overfitting Stop when: ✗ Validation loss flattens or increases ✗ Training loss keeps dropping but validation loss rises ✗ Early stopping triggers</div> <h3>What if training loss keeps dropping but validation loss increases?</h3> <p> This is classic overfitting. Solutions include: </p> <ul> <li>Add regularization (dropout, weight decay)</li> <li>Collect more training data</li> <li>Implement early stopping</li> <li>Reduce model complexity</li> </ul> <h3>How do I know if my learning rate is too high or low?</h3> <div class="comparison-table"> <div class="comparison-column"> <h5>Learning Rate Too High</h5> <ul> <li>Loss oscillates or spikes</li> <li>Gradients explode</li> <li>Training becomes unstable</li> </ul> </div> <div class="comparison-column"> <h5>Learning Rate Too Low</h5> <ul> <li>Loss crawls down slowly</li> <li>Training stalls early</li> <li>Very slow convergence</li> </ul> </div> </div> <div class="code-example"> Finding the Sweet Spot: 1. Plot learning rate on a log scale 2. Plot loss against learning rate 3. The sweet spot is the steepest descent before instability 4. Use learning rate schedulers for dynamic adjustment</div> <div class="summary-card"> <div class="summary-header"> <h3>Key Takeaways</h3> </div> <div class="summary-content"> <p>Effective ML training optimization requires balancing computational efficiency, proper profiling, and smart learning strategies. Always profile first to identify bottlenecks, then focus on model performance improvements. This systematic approach will save you both time and money.</p> </div> </div>]]></content><author><name></name></author><category term="engineering"/><category term="training"/><category term="systems"/><summary type="html"><![CDATA[FLOPs, profiling, and learning strategies for ML training optimization.]]></summary></entry><entry><title type="html">Building GPT from First Principles: Code and Intuition</title><link href="https://shekkari1999.github.io/blog/2025/building-gpt-from-first-principles/" rel="alternate" type="text/html" title="Building GPT from First Principles: Code and Intuition"/><published>2025-04-01T09:00:00+00:00</published><updated>2025-04-01T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/building-gpt-from-first-principles</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/building-gpt-from-first-principles/"><![CDATA[]]></content><author><name></name></author><category term="engineering"/><category term="training"/><category term="llm"/><summary type="html"><![CDATA[Building GPT from first principles with code and intuition.]]></summary></entry><entry><title type="html">From Quantization to Inference: Beginner’s Guide for Practical Finetuning</title><link href="https://shekkari1999.github.io/blog/2025/quantization-to-inference/" rel="alternate" type="text/html" title="From Quantization to Inference: Beginner’s Guide for Practical Finetuning"/><published>2025-04-01T09:00:00+00:00</published><updated>2025-04-01T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/quantization-to-inference</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/quantization-to-inference/"><![CDATA[]]></content><author><name></name></author><category term="engineering"/><category term="training"/><category term="llm"/><summary type="html"><![CDATA[A practical introduction to quantization, inference, and fine-tuning.]]></summary></entry><entry><title type="html">A Guide to Fine-tuning Methods in LLMs (Part 1)</title><link href="https://shekkari1999.github.io/blog/2025/fine-tuning-methods/" rel="alternate" type="text/html" title="A Guide to Fine-tuning Methods in LLMs (Part 1)"/><published>2025-03-17T09:00:00+00:00</published><updated>2025-03-17T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/fine-tuning-methods</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/fine-tuning-methods/"><![CDATA[<p> This blog explores various fine-tuning methods for Large Language Models. For a better understanding of the motivation behind these techniques, I recommend first reading my article on <a href="/blog/2025/memory-optimization/">Memory Optimization in Deep Learning</a>. </p> <h2>Understanding Fine-tuning vs Training</h2> <div class="note-block"> <p> <strong>Key Distinction:</strong> Training involves starting with random weights, while fine-tuning starts with pre-trained model weights. This fundamental difference shapes our approach to model adaptation. </p> </div> <h2>The Memory Challenge</h2> <p> When running large models on available hardware, you typically have two main options: </p> <div class="code-example"> Options for Running Large Models: 1. Reduce Memory Footprint • Quantization • Parameter-efficient methods • Gradient checkpointing 2. Distribute Computation • CPU offloading with DeepSpeed • Model parallelism • Pipeline parallelism</div> <h2>Evolution of Fine-tuning Approaches</h2> <h3>1. Full Fine-tuning</h3> <div class="diagram-container"> <img src="/assets/img/blog/fine-tuning-1/ft-1.png" alt="Full Fine-tuning Diagram" class="diagram"> <div class="diagram-caption">Figure 1: Traditional full fine-tuning approach where all model parameters are updated</div> </div> <p> In the early days of deep learning, when models had fewer parameters, full fine-tuning was the standard approach. This method updates all model weights during the adaptation process. </p> <h3>2. Partial Fine-tuning</h3> <p> As models grew larger, researchers began experimenting with partial fine-tuning, freezing about 10% of the model weights. While this reduced memory footprint, the performance benefits were limited. Meaningful improvements typically required fine-tuning at least 30% of the model parameters. </p> <blockquote> <p> The critical question emerged: Could we achieve significant performance improvements while updating only a tiny fraction of parameters? This led to the development of Parameter-Efficient Fine-Tuning (PEFT) methods. </p> </blockquote> <h3>Parameter-Efficient Fine-Tuning (PEFT)</h3> <p> The core idea behind PEFT is strategic: introduce small trainable neural networks called "adapters" at carefully selected locations within the model. These adapters act as learnable interfaces between the model's frozen layers. During fine-tuning, the original pre-trained weights remain unchanged, and only these adapter parameters are updated, making the process highly efficient. </p> <div class="comparison-table"> <div class="comparison-column"> <h5>Advantages</h5> <ul> <li>Parameter Efficient: Requires only a small fraction of trainable parameters</li> <li>Sample Efficient: Needs fewer examples for effective fine-tuning</li> <li>Memory Efficient: Significantly reduced memory footprint</li> </ul> </div> <div class="comparison-column"> <h5>Limitations</h5> <ul> <li>Increased Inference Latency: Additional computation overhead during forward pass</li> <li>Architecture Modifications: Requires changes to model structure</li> </ul> </div> </div> <div class="diagram-container"> <img src="/assets/img/blog/fine-tuning-1/ft-2.png" alt="PEFT Methods Overview" class="diagram"> <div class="diagram-caption">Figure 2: Overview of Parameter-Efficient Fine-Tuning approaches</div> </div> <h3>Understanding Prompt-Based Methods</h3> <h4>Hard Prompting</h4> <div class="code-example"> Example of Hard Prompting: Input: "How to make a delicious pizza?" Hard Prompt: "As an expert chef, provide step-by-step instructions for making a delicious pizza:" This is a discrete text prompt that guides the model's behavior but cannot be optimized during training.</div> <h4>Soft Prompting</h4> <div class="code-example"> Soft Prompt Example: Instead of discrete text, we use trainable embeddings: [0.23, -0.45, 0.89, ...] → Trained to represent "summarize" [0.67, 0.12, -0.34, ...] → Trained to represent "explain" These continuous vectors are learned during fine-tuning to optimize task performance.</div> <h4>Key Prompt-Based Methods</h4> <ul> <li><strong>Prefix Tuning:</strong> Adds trainable continuous tokens before specific layers</li> <li><strong>Prompt Tuning:</strong> Prepends trainable embeddings to the input</li> <li><strong>P-Tuning:</strong> Introduces trainable prompts at multiple positions</li> </ul> <div class="note-block"> <strong>Understanding Soft Prompts:</strong> <p> Think of soft prompts as the model learning a "language" of its own. For example, when fine-tuned on summarization tasks, the soft prompts might encode patterns that help the model recognize key information and generate concise outputs. While we can't "read" these embeddings directly, their effect on the model's behavior is measurable and consistent. </p> </div> <p> While these prompt-based methods showed promise, they haven't gained as much widespread adoption as more recent approaches like LoRA, which offers better efficiency and easier implementation. </p> <h3>Understanding SVD and Low-Rank Decomposition</h3> <p> Before diving into LoRA, let's understand the key concept behind it: low-rank matrix decomposition. Through a practical example using SVD (Singular Value Decomposition), we'll see how a large matrix can be represented using fewer parameters. This same principle is what makes LoRA efficient - it essentially adds a low-rank update (product of two smaller matrices) to the original weight matrix. </p> <div class="code-example"> # First, let's create a rank-deficient matrix import torch import numpy as np \_ = torch.manual_seed(0) # Generate a rank deficient matrix W d, k = 10, 10 r = 2 # we are defining a low rank W = torch.randn(d, r) @ torch.randn(r, k) # (10 _ 2) _ (2 _ 10) = (10 _ 10) print(W) </div> <div class="code-output"> tensor([[-1.0797, 0.5545, 0.8058, -0.7140, -0.1518, 1.0773, 2.3690, 0.8486, -1.1825, -3.2632], [-0.3303, 0.2283, 0.4145, -0.1924, -0.0215, 0.3276, 0.7926, 0.2233, -0.3422, -0.9614], [-0.5256, 0.9864, 2.4447, -0.0290, 0.2305, 0.5000, 1.9831, -0.0311, -0.3369, -1.1376], [ 0.7900, -1.1336, -2.6746, 0.1988, -0.1982, -0.7634, -2.5763, -0.1696, 0.6227, 1.9294], [ 0.1258, 0.1458, 0.5090, 0.1768, 0.1071, -0.1327, -0.0323, -0.2294, 0.2079, 0.5128], [ 0.7697, 0.0050, 0.5725, 0.6870, 0.2783, -0.7818, -1.2253, -0.8533, 0.9765, 2.5786], [ 1.4157, -0.7814, -1.2121, 0.9120, 0.1760, -1.4108, -3.1692, -1.0791, 1.5325, 4.2447], [-0.0119, 0.6050, 1.7245, 0.2584, 0.2528, -0.0086, 0.7198, -0.3620, 0.1865, 0.3410], [ 1.0485, -0.6394, -1.0715, 0.6485, 0.1046, -1.0427, -2.4174, -0.7615, 1.1147, 3.1054], [ 0.9088, 0.1936, 1.2136, 0.8946, 0.4084, -0.9295, -1.2294, -1.1239, 1.2155, 3.1628]]) </div> <div class="code-example"> # Let's verify the rank of our matrix W_rank = np.linalg.matrix_rank(W) print(f'The Rank of Matrix is: {W_rank}') </div> <div class="code-output"> The Rank of Matrix is: 2 </div> <p> As expected, the matrix has rank 2, confirming that all its information can be represented using just two dimensions, despite being a 10×10 matrix. This is a key insight into why low-rank methods work. </p> <div class="code-example"> # Performing SVD on W (U _ S _ V^T) U, S, V = torch.svd(W) U_r = U[:, :W_rank] S_r = torch.diag(S[:W_rank]) V_r = V[:, :W_rank].t() A = U_r @ S_r B = V_r print(A.shape, B.shape) </div> <div class="code-output"> torch.Size([10, 2]) torch.Size([2, 10]) </div> <p> SVD decomposes our matrix into three components, but remarkably, we only need to keep the first two singular values and their corresponding vectors. This is because these capture the essential structure of our matrix, while the remaining values are effectively zero. </p> <div class="code-example"> # Let's verify our decomposition works perfectly bias = torch.randn(d) x = torch.randn(d) y = W @ x + bias y_hat = (A @ B) @ x + bias print(f'Values with Original Weights: {y}\n\n') print(f'Values with (A \* B) Weights: {y_hat}') </div> <div class="code-output"> Values with Original Weights: tensor([-2.1548, 0.4832, 1.2947, -0.8374, 0.3158, 1.0483, 2.3690, 0.8486]) Values with (A \* B) Weights: tensor([-2.1548, 0.4832, 1.2947, -0.8374, 0.3158, 1.0483, 2.3690, 0.8486]) </div> <p> This example demonstrates the power of low-rank decomposition: we could perfectly replicate the behavior of the original weights using far fewer parameters. A and B combined have only 40 parameters (10×2 + 2×10 = 40), while the original W matrix had 100 parameters (10×10 = 100). This 60% reduction in parameters is exactly the kind of efficiency that makes LoRA so powerful. </p> <h3>Low-Rank Adaptation (LoRA)</h3> <p> The main idea of Low-Rank Adaptation (LoRA) is to decompose weight updates into low-rank matrices, train these smaller matrices, and then add their product back to the original weights. </p> <p> Let's break down how LoRA works: Instead of directly updating the large weight matrices of the model, LoRA introduces two smaller matrices (A and B) whose product approximates the weight update. This approach significantly reduces the number of trainable parameters while maintaining model quality. </p> <div class="diagram-container"> <img src="/assets/img/blog/fine-tuning-1/ft-4.png" alt="LoRA Matrix Decomposition" class="diagram"> <div class="diagram-caption">Figure 3: LoRA's low-rank matrix decomposition and update process</div> </div> <div class="code-example"> Mathematical Formulation of LoRA: Instead of learning the full weight update ΔW, LoRA decomposes it as: ΔW = α(A × B) // where α is the scaling factor (in our example, α = 1) // α determines how much weight to give to the LoRA update where: From the diagram example: • A is a matrix of shape (3 × 1): [1, 2, 3] • B is a matrix of shape (1 × 3): [0.5, 0.2, 0.1] • r = 1 (rank of decomposition) Final Weight Update: W = W₀ + ΔW where W₀ is the original weight matrix [1,2,3; 4,5,6; 7,8,9] Key Insight: With just 6 trainable parameters (3 in A + 3 in B), we can update a 3×3 matrix containing 9 weights! </div> <h4>Key Advantages of LoRA</h4> <p> Unlike traditional adapter methods that add extra layers and increase inference latency, LoRA's design offers a unique advantage: the trained matrices can be merged with the original weights at inference time, resulting in zero additional latency. </p> <h3>Serving LoRA Models</h3> <p> One of LoRA's most powerful features is its flexibility during inference. There are two main approaches: </p> <div class="comparison-table"> <div class="comparison-column"> <h5>Merged Weights</h5> <ul> <li>Add LoRA updates (A×B) back to original weights</li> <li>Zero inference overhead</li> <li>Same memory footprint as original model</li> <li>Best for single-task deployment</li> </ul> </div> <div class="comparison-column"> <h5>Separate Weights</h5> <ul> <li>Keep LoRA matrices separate</li> <li>Switch between different fine-tuned versions</li> <li>Combine multiple LoRA adaptations</li> <li>Ideal for multi-task scenarios</li> </ul> </div> </div> <p> This flexibility allows for interesting deployment scenarios. For example, you could have a base model with different LoRA adaptations for different languages or tasks, and dynamically choose or even combine them at inference time. </p> <div class="diagram-container"> <img src="/assets/img/blog/fine-tuning-1/ft-14.png" alt="LoRA Serving Options" class="diagram"> <div class="diagram-caption">Figure 4: Different approaches to serving LoRA models - merged vs separate weights</div> </div> <div class="note-block"> <p> <strong>Storage Benefits of Separate LoRA Weights: A Simple Example</strong> </p> <p> Let's say we have a small weight matrix W of size 3×3 (9 parameters) and 5 different customers: </p> <p> Option 1 (Merged Weights): • Store 5 full matrices (Wʹ) of size 3×3 • Total storage: 9 parameters × 5 = 45 parameters </p> <p> Option 2 (Separate LoRA): • Store 1 base matrix W (9 parameters) • Store 5 sets of LoRA matrices A(3×1) and B(1×3) • Each LoRA pair needs 6 parameters (3 + 3) • Total storage: 9 + (6 × 5) = 39 parameters </p> <p> Even in this tiny example, separate storage saves ~13% space. The savings become much more dramatic with real-world model sizes and more customers. </p> </div> <h3>QLoRA: Quantized LoRA</h3> <p> QLoRA combines the efficiency of LoRA with the memory benefits of quantization. It's a powerful approach that makes fine-tuning possible on consumer GPUs while maintaining model quality. </p> <div class="comparison-table"> <div class="comparison-column"> <h5>Key Components</h5> <ul> <li>Base model weights are frozen and quantized (typically to 4-bit)</li> <li>LoRA parameters remain in full precision (16-bit)</li> <li>Gradients computed in full precision during backpropagation</li> </ul> </div> <div class="comparison-column"> <h5>Why This Works</h5> <ul> <li>Most memory is in frozen weights - safe to quantize</li> <li>LoRA updates need precision for learning - kept in 16-bit</li> <li>Dequantization during forward pass preserves accuracy</li> </ul> </div> </div> <p> Thanks for sticking till the end! In Part 2, we'll explore advanced topics including model merging and multitask fine-tuning. Stay tuned! </p>]]></content><author><name></name></author><category term="engineering"/><category term="training"/><category term="llm"/><summary type="html"><![CDATA[A practical guide to fine-tuning methods for large language models.]]></summary></entry><entry><title type="html">Understanding Quantization in Deep Learning</title><link href="https://shekkari1999.github.io/blog/2025/memory-optimization/" rel="alternate" type="text/html" title="Understanding Quantization in Deep Learning"/><published>2025-03-13T09:00:00+00:00</published><updated>2025-03-13T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/memory-optimization</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/memory-optimization/"><![CDATA[<h2>Understanding Memory Footprint</h2> <p> When working with deep learning models, understanding memory usage is crucial. The total memory consumption can be broken down into two main components: </p> <div class="code-example"> Total memory = Training memory + Inference memory Training memory = Weights + Gradient memory + Optimizer memory + Activations Inference memory = Weights + Activation memory (forward pass)</div> <h3>Example: Calculating Model Memory</h3> <div class="code-example"> Let's calculate memory for a simple neural network: - Input layer: 784 neurons (28x28 image) - Hidden layer: 512 neurons - Output layer: 10 neurons (digits 0-9) Weights memory: - Layer 1: <span class="calculation">784 × 512 = 401,408 parameters</span> - Layer 2: <span class="calculation">512 × 10 = 5,120 parameters</span> <span class="result">Total parameters: 406,528</span> Using float32 (4 bytes): - Weights: <span class="calculation">406,528 × 4 = 1.6 MB</span> - Gradients: <span class="calculation">1.6 MB</span> - Optimizer (Adam, 2 states): <span class="calculation">3.2 MB</span> - Activations (batch size 32): <span class="calculation">~0.2 MB</span> <span class="result">Total Training Memory: ~6.6 MB</span> <span class="result">Inference Memory: ~1.8 MB</span></div> <h2>Memory Optimization Techniques</h2> <p> Let's explore key techniques for reducing memory usage in deep learning models, starting with an important but often overlooked approach: </p> <h3>1. Gradient Checkpointing</h3> <p> Gradient checkpointing is a powerful technique that trades computation time for memory savings. Instead of storing all activations in memory during the forward pass, we do the following: </p> <p> <strong>Strategy:</strong> </p> <ol> <li>Store activations at checkpoints only</li> <li>Recompute intermediate activations when needed</li> <li>Free memory after gradients are computed</li> </ol> <p> <strong>Trade-offs:</strong> </p> <ul class="trade-offs"> <li class="benefit">✓ Reduced memory footprint</li> <li class="drawback">✗ Increased training time (recomputation)</li> </ul> <h3>Basics and Lookup Table</h3> <div class="code-example"> Floating Point Formats Comparison: Format Bytes Precision Common Use Case ───────────────────────────────────────────── FP64 8 15-17 dec Scientific Computing (rare in DL) FP32 4 6-9 dec Training (standard) FP16 2 3-4 dec Inference/Training INT8 1 256 levels Quantized Inference INT4 0.5 16 levels Extreme Compression INT1 0.125 2 levels Experimental (e.g., Blackwell)</div> <h4>Understanding INT4 Range</h4> <p> When we say INT4 (4-bit integer) has a range of -8 to 7, we're describing the minimum and maximum values that can be represented using 4 bits in signed integer format. Let's break this down: </p> <div class="code-example"> 1. 4 Bits = 4 Binary Digits • Each bit can be either 0 or 1 • So, 4 bits can represent 2⁴ = 16 unique values 2. Signed vs. Unsigned • Unsigned INT4: represents positive values only Range: 0 to 15 • Signed INT4 (common in ML quantization) Uses Two's Complement representation Range: -8 to 7 Binary Representation of INT4 (Signed): Binary Decimal ───────────────── 1000 -8 (Most negative) 1111 -1 0000 0 0001 1 0111 7 (Most positive)</div> <div class="note-block"> <strong>Key Points:</strong> <ul> <li>Signed integers reserve one bit for the sign (positive/negative)</li> <li>Two's Complement allows efficient hardware implementation</li> <li>The range is asymmetric around zero (-8 to +7) due to Two's Complement</li> </ul> </div> <div class="note-block"> <strong>Important Note:</strong> While reducing precision can significantly decrease memory usage, it can also introduce numerical errors. Always validate model performance after precision reduction. </div> <h3>2. Understanding Quantization</h3> <div class="diagram-container"> <img src="/assets/img/blog/memory-optimization/Quant.png" alt="Quantization Impact Diagram" class="diagram"> <div class="diagram-caption">Figure: Quantization can be applied to four key areas: Weights, Training Time, Inference Time, and Activations</div> </div> <p>From the figure, we now understand that:</p> <ol> <li>Quantization can be applied to weights, Activations.</li> <li>It can also be applied in Inference time and Training time.</li> </ol> <p>Let's see one by one.</p> <h4>1. Quantizing Weights (Static and Stable)</h4> <p> Weights are ideal candidates for quantization because they change less frequently. Once trained, weights remain constant unless the model is fine-tuned, making them perfect for one-time quantization. </p> <div class="code-example"> Original Weights (FP32): [0.45, -0.23, 0.89, -0.75] The range of weights: min = -0.75, max = 0.89 Quantization Process: • The INT8 range is from -128 to 127 • Calculate the scaling factor (S): S = (Max - Min) / 255 = (0.89 - (-0.75)) / 255 ≈ 0.00647 Quantized weights (INT8) using: Q = round((Original - Min) / S) - 128</div> <div class="quantization-table"> <table> <thead> <tr> <th>Original Weight</th> <th>Quantized Value (INT8)</th> </tr> </thead> <tbody> <tr> <td>0.45</td> <td>92</td> </tr> <tr> <td>-0.23</td> <td>35</td> </tr> <tr> <td>0.89</td> <td>127</td> </tr> <tr> <td>-0.75</td> <td>-128</td> </tr> </tbody> </table> </div> <div class="note-block"> <strong>Key Benefits:</strong> <ul> <li>Weights only need to be quantized once after training</li> <li>Quantized model can be used repeatedly without re-quantization</li> <li>More predictable impact on model performance</li> </ul> </div> <h4>2. Quantizing Activations (Dynamic Values)</h4> <p> Unlike weights, activations change with every inference because they depend on the input data. This makes activation quantization more challenging and requires careful consideration of the dynamic range. </p> <div class="code-example"> Example: ReLU Activation Values for Different Inputs: Input Image 1 (digit 7): [0.0, 4.2, 0.0, 3.1, 0.0] Range: 0.0 to 4.2 Input Image 2 (digit 4): [2.1, 0.0, 5.7, 0.0, 1.9] Range: 0.0 to 5.7 Input Image 3 (digit 1): [0.0, 0.0, 7.2, 0.0, 0.0] Range: 0.0 to 7.2 Observation: • Activation ranges vary significantly between inputs • Need dynamic scaling for effective quantization • Common to use running statistics for range estimation</div> <div class="note-block"> <strong>Challenges with Activation Quantization:</strong> <ul> <li>Dynamic range varies with each input</li> <li>Requires runtime quantization/dequantization</li> <li>May need batch-wise statistics for better accuracy</li> <li>More sensitive to quantization errors than weights</li> </ul> </div> <h3>Inference Time Quantization</h3> <p> Inference time quantization focuses on serving the model in low precision to accelerate computation. Modern approaches have moved beyond simple quantization to mixed precision strategies, which offer a better balance between performance and accuracy. </p> <p> In a typical mixed precision setup: </p> <ul> <li>Model weights are stored in FP16 or FP8 format for memory efficiency</li> <li>Activations and gradients use FP32 or FP16 for better numerical stability</li> <li>Critical operations may dynamically switch between precisions as needed</li> </ul> <h3>Quantization-Aware Training (QAT)</h3> <p> QAT is a training-time technique designed to maintain high accuracy when models are deployed with low-bit quantization (like INT8, INT4). Unlike post-training quantization, QAT allows the model to adapt to quantization effects during the training process itself. </p> <h4>How QAT Works</h4> <div class="diagram-container"> <img src="/assets/img/blog/memory-optimization/quant-2.png" alt="Quantization-Aware Training Process" class="diagram"> <div class="diagram-caption">Figure: The QAT process showing fake quantization during training and real quantization for deployment</div> </div> <p>The QAT process involves four key steps:</p> <h5>1. Simulate Quantization During Training</h5> <p> During the forward pass, weights and activations are "fake quantized" to simulate deployment conditions. This involves rounding and clipping values based on the target precision (like INT8), helping the model learn to work within quantization constraints. </p> <div class="code-example"> Simple Example of How Learning Happens: 1. Original Weight (FP32): W = 0.45 2. Fake Quantized (during forward pass to INT8): • Using a scale of 0.1, the quantized value becomes: Q = round(0.45/0.1) = 4 • Dequantized back for calculations: Q_dequantized = 4 × 0.1 = 0.4 3. Forward Pass Calculation (with Quantization Noise): • Let's say the model predicts an output based on 0.4 and calculates a loss 4. Loss Function Result: Loss = 0.2</div> <h5>2. Backpropagate Using High Precision</h5> <p> The backward pass maintains high-precision gradients (typically FP32) to ensure accurate learning. This dual approach allows stable gradient updates while still preparing the model for quantized deployment. </p> <div class="code-example"> Example: High-Precision Gradient Calculation Given from previous step: • Original weight (W) = 0.45 • Quantized forward value = 0.4 • Loss = 0.2 Backward Pass (in FP32): • Gradient = ∂Loss/∂W = -0.15 • Learning rate (η) = 0.01 Weight Update: W_new = W - η × gradient W_new = 0.45 - 0.01 × (-0.15) W_new = 0.4515 (kept in FP32 during training)</div> <div class="note-block"> <strong>Key Insights:</strong> <ul> <li>The model adapts during training to minimize the accuracy loss that might occur from quantization</li> <li>It learns to "expect" the noise from quantization and adjusts accordingly</li> <li>Once training is complete, the model weights are actually quantized to low-bit precision for deployment</li> </ul> </div> <h4>Common Challenges</h4> <p> When implementing QAT, teams typically face several challenges: </p> <ul> <li>Balancing training time with quantization accuracy</li> <li>Choosing appropriate quantization parameters</li> <li>Handling layers with different sensitivity to quantization</li> <li>Managing the increased complexity of the training pipeline</li> </ul> <h3>What's Next?</h3> <p> In our next article, we'll explore advanced memory-efficient techniques like LoRA (Low-Rank Adaptation) and other parameter-efficient fine-tuning methods that are revolutionizing how we train large language models. </p> <div class="note-block"> <p> Did you find this article helpful? Have questions about implementing these techniques? I'd love to hear your thoughts and experiences in the comments below! Your feedback helps make these explanations better for everyone. </p> </div>]]></content><author><name></name></author><category term="engineering"/><category term="inference"/><category term="llm"/><summary type="html"><![CDATA[An introduction to quantization and memory optimization in deep learning.]]></summary></entry><entry><title type="html">From Scratch Implementation of ResShift Paper for Image Super-Resolution</title><link href="https://shekkari1999.github.io/blog/2025/resshift-from-scratch/" rel="alternate" type="text/html" title="From Scratch Implementation of ResShift Paper for Image Super-Resolution"/><published>2025-03-10T09:00:00+00:00</published><updated>2025-03-10T09:00:00+00:00</updated><id>https://shekkari1999.github.io/blog/2025/resshift-from-scratch</id><content type="html" xml:base="https://shekkari1999.github.io/blog/2025/resshift-from-scratch/"><![CDATA[<div class="placeholder-notice"> <h3>🚧 Blog Post Coming Soon</h3> <p> This blog post will dive deep into my complete from-scratch implementation of the ResShift paper for image super-resolution. I'll explain how I built an efficient diffusion-based super-resolution model using a U-Net architecture with Swin Transformer blocks, including the residual shifting mechanism that reduces diffusion steps to just 15 timesteps. The post will cover architecture design, training on DIV2K dataset, and lessons learned from implementing this state-of-the-art approach to image enhancement. </p> <p style="margin-top: 15px; font-style: italic;"> Check out the implementation: <a href="https://github.com/shekkari1999/DiffusionSR" target="_blank" style="color: #667eea;">GitHub Repository</a> </p> <p style="margin-top: 15px; font-style: italic;"> Stay tuned for detailed explanations, code walkthroughs, and practical insights! </p> </div> <div class="disclaimer"> <p> <strong>Note:</strong> This is a placeholder for a future blog post. The full content will be published soon. </p> </div>]]></content><author><name></name></author><category term="engineering"/><category term="training"/><category term="diffusion"/><summary type="html"><![CDATA[Implementing ResShift image super-resolution from scratch.]]></summary></entry></feed>