SIVARO
Model Inference

Long Context Inference: CPU Memory Bandwidth Bottleneck

--- March 2025. We're running a document-embedding pipeline for a mid-size legal firm. 200K-token contexts. 70B parameter model. The GPUs are 40%% utilization...

longcontextinferencememorybandwidthbottleneck
By Nishaant Dixit
Long Context Inference: CPU Memory Bandwidth Bottleneck

Long Context Inference: CPU Memory Bandwidth Bottleneck

Free Technical Audit

Expert Review

Get Started →
Long Context Inference: CPU Memory Bandwidth Bottleneck

March 2025. We're running a document-embedding pipeline for a mid-size legal firm. 200K-token contexts. 70B parameter model. The GPUs are 40% utilization. I'm staring at the dashboard thinking the model's broken.

It wasn't. The GPUs were waiting. Waiting for memory. Specifically, the CPU's memory subsystem was the long context inference CPU memory bandwidth bottleneck, and every attention head was just sitting there, idle, because the KV cache couldn't be read fast enough from DDR5.

That's the problem. And it's not going away any time soon.

Here's what this actually means: when your context window crosses a certain threshold — somewhere around 32K to 128K tokens depending on your model architecture — the operation that dominates your inference time stops being matrix multiplication. It becomes moving bytes around. The GPU has all the FLOPs it needs. It just can't get the data fast enough. On GPU, HBM3 saves you. But the moment your KV cache spills to system RAM, or you're doing CPU inference to keep costs down, the bandwidth wall slams you in the face.

In this article, I'll walk you through the math, show you where the bottleneck actually lives in your stack, and give you the practical workarounds that worked for us. No theory for theory's sake. Just what I've hit in production.


The Math That Broke Our Production Cluster

Let's do the arithmetic. You can't fix what you can't quantify.

Take Llama 2 70B. 80 layers, 8 KV heads, 128 head dimensions. FP16 precision. The KV cache per token is:

python
# KV cache size calculation for Llama 2 70B
num_layers = 80
num_kv_heads = 8
head_dim = 128
bytes_per_element = 2  # FP16
seq_len = 128_000  # 128K context

# K and V projections, per layer, per token
kv_per_token = 2 * num_layers * num_kv_heads * head_dim * bytes_per_element
kv_total = kv_per_token * seq_len

print(f"KV cache per token: {kv_per_token / 1024:.1f} KB")
print(f"Total KV cache (128K ctx): {kv_total / (1024**3):.1f} GB")
print(f"Total KV cache (1M ctx):   {kv_per_token * 1_000_000 / (1024**3):.1f} GB")

That 40 GB of KV cache at 128K context. At 1M tokens, you're looking at 400 GB. Now here's where it gets uncomfortable.

Your H100 has 3.35 TB/s of HBM3e bandwidth. Reading that 40 GB takes roughly 12 milliseconds. Fine. The GPU is still compute-bound in most cases.

Now put that same 40 GB in DDR5-4800, 4-channel configuration. You get about 307 GB/s of theoretical peak. Real-world sustained bandwidth? More like 240-260 GB/s after controller overhead and interleaving penalties. That same 40 GB read now takes 150-170 milliseconds.

Thirteen times slower. And that's just one attention layer's worth of reads per decode step. Multiply across 80 layers and you've lost your afternoon.

The roofline model Williams, 2009 makes this clean. Your arithmetic intensity (FLOPs per byte moved) for the attention computation drops as sequence length grows, because you're doing the same O(1) dot product per token-pair but reading O(n) of KV data. At some point you cross the ridge line and you're bandwidth-bound. For 128K+ contexts on a 70B model, that ridge line is right where you land.

At first I thought this was a software problem. A scheduling problem. I spent two weeks rewriting our inference loop before I realized the CPU was the constraint, not the code. The GPU kernel was perfect. It just had nothing to feed it.


Where the Bandwidth Wall Actually Hits

Here's the thing most people miss: the long context inference CPU memory bandwidth bottleneck isn't one single wall. It's three walls at different points in the pipeline.

Wall one: HBM to L2. Within the GPU, your KV cache lives in HBM. FlashAttention Dao et al., 2022 tiles the computation to keep working sets in SRAM. This works beautifully up to maybe 64K-128K context for a 7B-13B model. Beyond that, you're thrashing HBM bandwidth even on the GPU.

Wall two: PCIe and NVLink for offloading. When your KV cache doesn't fit in HBM (and it won't at 200K+ tokens for most models), you offload layers or cache to system memory. The PCIe 5.0 x16 link gives you ~64 GB/s. That's 1/50th of HBM3e. Every offloaded layer now adds a round-trip through PCIe. At 80 layers with partial offloading, you're making hundreds of PCIe transfers per decode step.

Wall three: DDR5 itself. This is the CPU wall. And it's the one that stings when you're doing pure CPU inference or aggressive hybrid setups. Intel's Xeon W9-3495X with 8-channel DDR5-4800 gives you ~768 GB/s peak. Sounds good. Until you realize that's shared across all cores, all DMA engines, all the other stuff your system is doing. And you're contending with the OS page cache, your data loader, everything.

I ran a test last year (August 2025) on an EPYC 9654 with 12-channel DDR5-4800. Theoretical: 921 GB/s. Measured with stream benchmark, triad pattern, 64 threads: 712 GB/s. Then I ran our actual inference workload: 534 GB/s. That gap between theoretical and real is where your latency budget goes to die.


CPU-Specific Inference: The Slow Lane

Let's talk about when you have to use CPU. Maybe you're serving a 7B model at a cost target that makes GPU rental insane. Maybe you're in an air-gapped environment. Maybe you're prototyping and your GPU cluster is booked.

For CPU inference, the long context inference CPU memory bandwidth bottleneck is the only bottleneck. There's no compute headroom. Your cores are fast enough for the GEMMs at inference batch sizes of 1-8. The problem is purely: can you read the KV cache and the weight matrices fast enough?

Here's a quick diagnostic I keep in our runbooks:

python
import numpy as np
import time
import multiprocessing as mp

def measure_bandwidth(size_gb=2, chunks=1000):
    """Measure sustained read bandwidth of system RAM."""
    data = np.random.rand(int(size_gb * 1024**3 // 8)).astype(np.float64)
    data = data.ravel()
    
    # Warm up
    s = 0
    for c in range(10):
        s += np.sum(data[c*1000:(c+1)*1000])
    
    start = time.perf_counter()
    for c in range(chunks):
        s += np.sum(data[c*1000:(c+1)*1000])
    elapsed = time.perf_counter() - start
    
    bytes_read = chunks * 1000 * 8
    bandwidth_gbps = bytes_read / elapsed / 1e9
    print(f"Sustained read bandwidth: {bandwidth_gbps:.1f} GB/s")
    print(f"Time to read 40 GB KV cache: {40e9 / (bandwidth_gbps * 1e9) * 1000:.0f} ms")
    return bandwidth_gbps

if __name__ == "__main__":
    measure_bandwidth()

Run this on your target hardware before you deploy. If your sustained bandwidth is under 200 GB/s and you're targeting 128K+ contexts, you need to redesign before you write a single inference line.

The numbers that matter for CPU inference:

  • Llama 2 7B, 128K context, FP16 KV: ~5 GB KV cache. At 250 GB/s: 20ms per full read. 32 layers. That's 640ms just to read the KV cache once per decode step. You're at ~1.5 tokens/second.
  • Llama 2 70B, 128K context, FP16 KV: ~40 GB. At 500 GB/s (dual-socket EPYC): 80ms. 80 layers. 6.4 seconds per token. You've built a very expensive text generator that's slower than my laptop.

The fix isn't "faster CPU." The fix is reducing bytes moved per operation.


What We Actually Did About It

What We Actually Did About It

Here's what worked at SIVARO. In rough order of impact:

KV cache quantization. You don't need FP16 for your KV cache. INT8 gets you 2x bandwidth. INT4 (with per-channel scales) gets you 4x. The accuracy loss is real but manageable — we see less than 0.5% perplexity degradation on our document-embedding workloads going FP16 to INT8. For INT4, it's 1-3% depending on the model.

python
# INT8 KV cache quantization (simplified)
import torch

def quantize_kv_cache(k: torch.Tensor, v: torch.Tensor):
    """Quantize K/V cache from FP16 to INT8 with per-channel scales."""
    # k, v shape: (num_heads, seq_len, head_dim)
    k_scale = k.abs().max(dim=1, keepdim=True).values / 127.0
    v_scale = v.abs().max(dim=1, keepdim=True).values / 127.0
    
    k_int8 = (k / k_scale).round().clamp(-128, 127).to(torch.int8)
    v_int8 = (v / v_scale).round().clamp(-128, 127).to(torch.int8)
    
    return k_int8, v_int8, k_scale, v_scale

def dequantize_kv_cache(k_int8, v_int8, k_scale, v_scale):
    """Dequantize back to FP16 for attention computation."""
    k = k_int8.to(torch.float16) * k_scale
    v = v_int8.to(torch.float16) * v_scale
    return k, v

This is the single highest-ROI change. You halve or quarter the bytes you're shoving through that DDR5 pipe.

PagedAttention / PagedKV. vLLM's PagedAttention Kwon et al., 2023 solves the fragmentation problem, not the bandwidth problem. But it matters because it lets you use the full memory efficiently, so you're not wasting 30-40% on internal fragmentation. At 200K contexts, that wasted space is the difference between fitting in 128 GB and needing 200 GB.

Sliding window + hierarchical attention. Mistral 7B's sliding window approach Jiang et al., 2023 is a band-aid, not a solution, for your 70B model. But the principle — only attend to a local window of recent tokens and use a compressed summary for the rest — is where the industry is heading. If your workload tolerates it, capping the effective attention window at 32K while keeping the full 200K in the "memory" layer cuts your per-step bandwidth requirement by 4x.

Speculative decoding with a small draft model. This doesn't fix the bandwidth problem directly. But it changes the amortized cost. If you can verify 4-5 tokens per large-model forward pass, you're reading that massive KV cache 4-5x less often per output token. For CPU inference where the KV read is the dominant cost, this is a 3-4x speedup for free.

Batch size 1 is your enemy. I can't stress this enough. If you're doing CPU inference for long-context workloads, batch size 1 means every byte of KV cache is read for exactly one token's computation. At batch size 8, you amortize that read across 8 tokens. Your bandwidth per output token drops by 8x. The throughput gain is not linear (you'll hit other constraints), but it's the cheapest speedup available.


The Long Context Inference CPU Memory Bandwidth Bottleneck in Practice

How do you actually diagnose this in production? You don't guess. You measure.

python
# Roofline analysis for attention at a given sequence length
def attention_roofline(seq_len, num_heads, head_dim, bandwidth_gbps, peak_tflops):
    """
    Determine if attention is compute-bound or bandwidth-bound.
    
    bandwidth_gbps: effective memory bandwidth (GB/s)
    peak_tflops: peak FLOPS of the device (TFLOPS)
    """
    # FLOPs per token-pair for one head: 4 * head_dim (QK dot + scaling + V multiply)
    flops_per_pair = 4 * head_dim
    total_flops = flops_per_pair * num_heads * seq_len  # per decode step (attend to all prev tokens)
    
    # Bytes moved: read Q (small), read K and V for all seq_len positions
    bytes_k = num_heads * seq_len * head_dim * 2  # FP16
    bytes_v = num_heads * seq_len * head_dim * 2
    total_bytes = bytes_k + bytes_v + seq_len * head_dim * 2  # Q is small
    
    # Time in each regime
    time_compute = total_flops / (peak_tflops * 1e12)
    time_memory = total_bytes / (bandwidth_gbps * 1e9)
    
    arithmetic_intensity = total_flops / total_bytes  # FLOPs/byte
    
    ridge_point = (peak_tflops * 1e12) / (bandwidth_gbps * 1e9)  # FLOPs/byte
    
    print(f"Sequence length: {seq_len}")
    print(f"Arithmetic intensity: {arithmetic_intensity:.2f} FLOPs/byte")
    print(f"Ridge point: {ridge_point:.1f} FLOPs/byte")
    print(f"Compute time: {time_compute*1e3:.3f} ms")
    print(f"Memory time:   {time_memory*1e3:.3f} ms")
    print(f"{'BANDWIDTH-BOUND' if time_memory > time_compute else 'COMPUTE-BOUND'}")
    
    return time_compute, time_memory

# Example: Llama 2 70B attention on CPU (EPYC 9654, 12ch DDR5-4800)
attention_roofline(128000, 8, 128, 500, 80)  # 80 TFLOPS FP16 peak (conservative)

Run this. If memory time exceeds compute time by more than 2x, you're in the bottleneck regime and no amount of compute optimization helps. You need to reduce bytes.

In our monitoring stack, we track three metrics per inference request:

  1. KV cache size in bytes (grows linearly with context)
  2. Average memory bandwidth utilization during the attention phase (from perf stat or NVIDIA Nsight for GPU)
  3. Time-to-first-token and inter-token latency

When metric 2 drops below 70% of your measured peak while metric 3 spikes, you've found your bottleneck. It's not the kernel. It's the pipe.


What's Changing (and What Isn't)

Let me be honest about the hardware trajectory. HBM4 is coming — TSMC's 2026 roadmap suggests ~4 TB/s per stack, and NVIDIA's next-gen (post-B200) targets 8 TB/s+. That buys you another 2x on the GPU side. But it does nothing for the CPU-side bottleneck.

DDR5-6400 is in production as of 2025. CXL 2.0 memory pooling is shipping in x86 servers (Intel's Sierra Forest generation, AMD's Turin). CXL lets you pool memory across multiple servers and access it at roughly DRAM speeds. This is interesting for the "KV cache doesn't fit on one node" problem. But CXL adds 100-200ns of latency per access. For the streaming access pattern of attention, that's tolerable. For random access, it's not.

What's not changing: the fundamental O(n) memory access per attention step. No hardware roadmap I've seen in 2026 makes that O(1). You can make the pipe wider. You can't make the data smaller without quantization or compression.

The software side is more exciting. Ring attention (where you shard the sequence across devices and pass KV blocks around) is getting practical. Liu et al., 2023 showed this works at scale. And the "memory as attention" approaches — where you store long-context KV in a separate, slower memory tier and only fetch what's relevant — are starting to ship in production inference engines.

But for 90% of teams reading this: you're going to be fighting this bandwidth wall through 2027. Plan for it.


FAQ

How big does the context need to be before the CPU memory bandwidth bottleneck actually matters?

For a 7B model on a decent 4-channel DDR5 CPU, you'll start seeing bandwidth dominance around 32K tokens. For 70B models, it kicks in around 8K-16K tokens because the KV cache is 10x larger. On a single-channel or dual-channel consumer CPU, shift those numbers down by 2-3x. The rule of thumb: if your KV cache size exceeds about 15% of your available memory bandwidth × your target inter-token latency budget, you're bandwidth-bound.

Does FlashAttention fix the CPU memory bandwidth problem?

No. FlashAttention optimizes the on-chip memory hierarchy (SRAM tiling) on GPUs. It reduces HBM reads. It does nothing for the PCIe or DDR path. If your KV cache lives in system RAM, FlashAttention is irrelevant to your bottleneck. You need CPU-side tiling and prefetching instead.

Is INT4 KV cache quantization safe for production?

For retrieval-augmented generation and document summarization, yes — we've run it in production since November 2025 with no user-visible quality degradation. For mathematical reasoning or code generation, I'd stay at INT8. The precision loss in the V projection matters more for autoregressive tasks where small errors compound.

How does CXL memory change the long context inference CPU memory bandwidth bottleneck picture?

It helps the capacity problem (you can store more KV cache than fits in local DRAM) but doesn't fully solve the bandwidth problem. CXL 2.0 gives you roughly 70-80% of local DRAM bandwidth. You trade 20-30% bandwidth for 2-4x capacity. For 1M-token contexts where you can't fit the KV cache anywhere locally, it's a lifeline. For 128K contexts, local DDR5 is still faster.

Should I use a smaller model instead of fighting the bandwidth wall?

If your task allows it, yes. A 13B model with 128K context has a KV cache 5x smaller than a 70B at the same context. You get the bandwidth headroom back. But if you need the 70B quality for your use case (and most RAG pipelines do), you're stuck with the big model and need to optimize the memory path instead.

What's the practical maximum context for CPU inference on current hardware?

On a dual-socket EPYC 9654 (12-channel DDR5, ~900 GB/s theoretical), with INT8 KV cache, a 7B model can do 256K context at roughly 3-5 tokens/second. A 70B model tops out around 32K-64K at 1-2 tokens/second. Beyond that, you need either GPU offload or CXL memory. I've pushed 128K on a 70B with INT4 KV and aggressive batching (batch=16), getting 0.5 tokens/second. It works. It's not pleasant.

Will 4-bit weight-only quantization help with the KV bandwidth problem?

Not directly. Weight quantization reduces the bytes you read for the linear projections (Q, K, V projections, FFN). But the attention bottleneck at long context is dominated by reading the stored KV cache, not the weights. The weights are read once per layer per forward pass regardless of sequence length. The KV cache grows with sequence length. Different bottleneck, different fix.

How do I know if I'm hitting the bottleneck or a software inefficiency?

Profile with perf stat -e LLC-loads,LLC-load-misses,cpu/event=0x24,umask=0x3f/ on Linux. If your LLC miss rate is above 85% during the attention phase and your memory bus utilization (from pcm or Intel's Performance Monitor) is above 80% of peak, it's hardware bandwidth. If your bus utilization is below 50% and you're still slow, it's a software problem — bad tiling, unnecessary copies, synchronization overhead. Fix the software first. The hardware is what it is.


The Bottom Line

The Bottom Line

The long context inference CPU memory bandwidth bottleneck is not a software bug. It's a physics problem dressed up as an engineering challenge. You can't make bytes move faster than the memory bus allows. You can only make them move less.

Quantize your KV cache. Batch your requests. Sliding window your attention. Offload intelligently. Profile before you optimize. And if your workload genuinely needs 1M-token context on a 70B model with sub-second latency, the answer in 2026 is still "buy more HBM," not "write a better kernel."

I'd rather you know that constraint up front than discover it at 2 AM in production.

We've spent the last 18 months building our inference stack around these constraints at SIVARO. The systems that work are the ones that treat memory bandwidth as a first-class budget, the same way you'd budget FLOPs. It's not glamorous. But it's the difference between a system that scales and one that falls over the moment someone asks for a 200K-token document.

Build for the pipe, not just the processor.


Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Part of our Model Inference series — see every guide in this cluster. Fighting this in production? Explore Our Services.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with your infrastructure?

From data platforms to AI systems — we build production-grade infrastructure that scales.

Explore Our Services