<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
>
<channel>
  <title>Momento Japan Feed</title>
  <atom:link href="https://www.gomomento.com/jp/feed" rel="self" type="application/rss+xml" />
  <link>https://www.gomomento.com/jp/</link>
  <description>Momentoの日本向け更新情報。</description>
  <lastBuildDate>Fri, 26 Jun 2026 00:00:00 GMT</lastBuildDate>
  <language>ja</language>
  <sy:updatePeriod>hourly</sy:updatePeriod>
  <sy:updateFrequency>1</sy:updateFrequency>
  <image>
    <url>https://www.gomomento.com/wp-content/uploads/2024/06/cropped-favicon-green-32x32.png</url>
    <title>Momento Japan Feed</title>
    <link>https://www.gomomento.com/jp/</link>
    <width>32</width>
    <height>32</height>
  </image>
  <item>
    <title>The concurrency cliff is a memory limit</title>
    <link>https://www.gomomento.com/jp/blog/the-concurrency-cliff-is-a-memory-limit/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/the-concurrency-cliff-is-a-memory-limit/</guid>
    <description><![CDATA[<p>Latency looks fine on p50 until the KV cache fills. Then p99 jumps 15x in one step while your average dashboard shows nothing wrong. The cliff is memory.</p>]]></description>
    <content:encoded><![CDATA[<p>Add concurrent users to an inference server and latency usually creeps up. KV cache serving does not creep. It holds flat, then falls off a cliff.</p>
<p>On a single L4 running a Qwen3-4B coding agent, the server holds 12 concurrent sessions at p99 under 2.6 seconds. Add two more and p99 jumps to 39 seconds, a 15× increase in a single step. The cliff is the moment the KV cache fills.</p>
<h2 id="an-agentic-coding-workload-on-commodity-hardware">An agentic coding workload on commodity hardware</h2>
<p>Our setup is a single <code>g6.4xlarge</code> EC2 instance with one NVIDIA L4 GPU (24 GB VRAM), running <a href="https://huggingface.co/Qwen/Qwen3-4B">Qwen3-4B</a> with FP8 weights on vLLM 0.20.2, automatic prefix caching (APC) enabled.</p>
<p>The workload models a lightweight coding agent mid-task. Each session opens with a 10,000-token shared system prompt (repository context plus agent instructions), followed by a 12-turn conversation where each turn appends about 1,000 tokens of unique context (tool call inputs, code snippets, responses). At turn 12, total context reaches roughly 22,800 tokens per session. Output is capped at 75 tokens per turn. The workload is heavily input-dominated, as agentic workloads tend to be.</p>
<pre><code>System prompt:          10,000 tokens  (shared across sessions → APC cached)
Per-session turns:      ~12,800 tokens across 12 turns  (unique per session)
Output per turn:        75 tokens
—————————————————
Total at turn 12:       ~22,800 tokens
</code></pre>
<p>APC caches the system prompt once and amortizes its KV cost across all sessions (the blocks still occupy GPU memory, but only one copy exists). Within a session, APC also caches the growing turn history. Turn n+1 extends the exact prefix from turn n, so each turn only prefills the new ~1,000 tokens, as long as the prior turns’ KV blocks survive in cache. But the per-session turn history is unique (different code, different tool outputs) and cannot be shared across sessions. When concurrency pressure forces eviction of a session’s blocks, the next request in that session has to re-prefill the full accumulated history (up to ~12,800 tokens at turn 12). That re-prefill cost drives the TTFT cliff.</p>
<p>We swept concurrency from 1 to 48 sessions, measuring TTFT at each level, across three KV cache precisions, fp16, fp8, and TurboQuant 4-bit. We also ran best-case (all context cached) and worst-case (all context re-prefilled) bounds to bracket where realistic performance should land.</p>
<h2 id="the-concurrency-cliff">The concurrency cliff</h2>
<p>The chart below shows TTFT percentiles (p50, p95, p99) and throughput across the full concurrency sweep with fp8 KV cache. The Y axis is logarithmic, and even on a log scale the cliff is sharp.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-percentiles.avif" alt="chart: TTFT p50/p95/p99 + throughput vs concurrent sessions, fp8 KV">
<em>TTFT p50, p95, and p99, with throughput on the right axis, versus concurrent sessions. fp8 KV cache, Qwen3-4B FP8 on a g6.4xlarge (L4). The shaded band marks the 12 to 16 session collapse, and the dashed line marks the KV cache filling at 14c.</em></p>
<p>Below 12 concurrent sessions, p99 TTFT stays under 2.6 seconds and throughput climbs to a peak of 1.36 req/s. At 14 sessions, p99 jumps to 38.9 seconds in a single step, a 15× increase, and throughput drops 23 percent at the same time.</p>
<p>At 12 sessions, everything fits in KV cache. At 14, filling it forces eviction of another session’s blocks. That evicted session has to re-prefill its full 12,800 tokens of unique context at its next turn, and the cascade collapses latency.</p>
<p>The cliff sits between 12 and 14 sessions, and throughput never recovers past the peak at 12c. For a 2-second p99 SLA, the safe operating point is 6 concurrent sessions.</p>
<h3 id="after-the-cliff-p50-lies-to-you">After the cliff, p50 lies to you</h3>
<p>Above the cliff, p50 and p99 live in different regimes. At 20c, p50 is 2.2 seconds, which looks manageable, while p99 is 44.8 seconds, which is not. The distribution is bimodal, because APC creates two populations of requests. Lucky requests hit warm cache entries for their session context, prefill only the latest turn, and finish fast. Unlucky requests arrive after their session’s blocks were evicted, re-prefill the full 12,800 tokens, and take 30 to 50 seconds under load.</p>
<p>The p50 reflects the lucky cohort and the p99 reflects the unlucky one. A single TTFT average is meaningless past the cliff. You have to look at the tail to see the failure.</p>
<p>A few points from the sweep show the whole shape, flat through 12 sessions then the cliff at 14 and the widening p50/p99 gap past it.</p>





























































<table><thead><tr><th>Sessions</th><th>TTFT p50</th><th>TTFT p95</th><th>TTFT p99</th><th>req/s</th></tr></thead><tbody><tr><td>1</td><td>388</td><td>487</td><td>493</td><td>0.42</td></tr><tr><td>6</td><td>1.06s</td><td>1.43s</td><td>1.75s</td><td>1.18</td></tr><tr><td>10</td><td>1.19s</td><td>1.89s</td><td>2.31s</td><td>1.33</td></tr><tr><td>12</td><td>1.25s</td><td>2.16s</td><td>2.60s</td><td>1.36</td></tr><tr><td>14</td><td>1.29s</td><td>8.93s</td><td>38.9s</td><td>1.05</td></tr><tr><td>20</td><td>2.25s</td><td>25.5s</td><td>44.8s</td><td>0.68</td></tr><tr><td>48</td><td>5.74s</td><td>40.5s</td><td>43.2s</td><td>0.89</td></tr></tbody></table>
<h2 id="kv-cache-precision-moves-the-knee">KV cache precision moves the knee</h2>
<p>Running the same workload with 16-bit KV cache (vLLM defaults to the model’s dtype, bf16 for Qwen3, when <code>--kv-cache-dtype</code> is not set) halves the token capacity, and the knee shifts left in proportion.</p>

































<table><thead><tr><th>KV dtype</th><th>Bits/element</th><th>Token capacity</th><th>Knee (sessions)</th><th>2s p99 ceiling</th></tr></thead><tbody><tr><td>bf16/fp16</td><td>16</td><td>~89K</td><td>~8</td><td>~4</td></tr><tr><td>fp8</td><td>8</td><td>~178K</td><td>~14</td><td>~6</td></tr><tr><td>TurboQuant 4-bit</td><td>~4.2</td><td>~275K</td><td>~23 (est.)</td><td>pending</td></tr></tbody></table>
<p>The fp16 to fp8 shift is confirmed, with fp16 knees at about 8 and fp8 at about 14, a 1.75× shift for a 2× capacity increase. The slight compression below 2× is expected, since KV management overhead and block table fragmentation consume some of the headroom regardless of precision.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-p99.avif" alt="chart: TTFT p99, fp8 KV vs fp16 KV, knees marked">
<em>TTFT p99 for fp8 KV against fp16 KV, same workload and GPU. The markers sit at the observed knees, 8 sessions for fp16 and 14 for fp8. The tq4 knee near 23 is a capacity-based estimate, pending the experiment.</em></p>
<p>Quantization buys concurrency headroom directly. Halving KV precision from fp16 to fp8 nearly doubles how many concurrent sessions fit before the cliff. TurboQuant 4-bit (about 3.8× fewer bytes per element than fp16, partially offset by the lower <code>gpu_memory_utilization</code> it needs for autotuning scratch space) predicts a knee at about 23c, roughly 3× more concurrent sessions than fp16.</p>
<p>The accuracy tradeoff may be small. KV cache quantization at 4-bit typically reports low single-digit perplexity impact, though the exact effect depends on model and task. The knee shifts from about 8 sessions (fp16) to about 14 (fp8) to an estimated 23 (tq4), roughly 3× more sessions before eviction onset, from the same GPU.</p>
<h2 id="best-case-worst-case-realistic">Best case, worst case, realistic</h2>
<p>To separate the latency budget that is fundamental (prefill compute) from the part that is avoidable (cache misses), we ran two controlled bounds alongside the realistic workload.</p>
<p>In the best case (<code>miss_rate=0.0</code>), every request hits the same cached content. APC holds the full 12,800-token session context, so only about 200 unique tokens need prefilling, which is perfect KV utilization.</p>
<p>The worst case (<code>miss_rate=1.0</code>) gives every request a unique prefix that breaks APC for the user context. The 10K system prompt still hits the cache, but all ~12,800 tokens of per-session turn history are re-prefilled on every request. Every miss lands at peak session depth (turn 12), forcing the maximum re-prefill cost each time.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-p50.avif" alt="chart: TTFT p50, best vs realistic vs worst, log scale">
<em>TTFT p50 for best (fully cached), realistic (natural APC), and worst (always re-prefilled), on a log scale. The band between best and worst is the envelope any real workload lands in.</em></p>
<h3 id="what-the-bounds-tell-us">What the bounds tell us</h3>
<p>At a single concurrent session, with zero contention, the three bounds separate cleanly.</p>

























<table><thead><tr><th>Workload</th><th>TTFT p50</th><th>What’s happening</th></tr></thead><tbody><tr><td>Best (cached)</td><td>57 ms</td><td>Only ~200 unique tokens prefilled; rest is cached</td></tr><tr><td>Realistic (APC)</td><td>388 ms</td><td>System prompt cached; 12,800 unique tokens prefilled</td></tr><tr><td>Worst (evicted)</td><td>2,500 ms</td><td>System prompt cached; ~12,800 user tokens re-prefilled at peak depth every request</td></tr></tbody></table>
<p>On an absolute scale, realistic (388 ms) is much closer to best (57 ms) than to worst (2,500 ms). But realistic is still 7× slower than best. That gap is the cost of prefilling about 12,800 tokens of per-session unique context on each request. APC removes the system prompt cost, but the per-session turn history still has to be computed.</p>
<p>The gap between realistic and worst is about miss depth. In the realistic workload, cache misses happen at any turn. A session evicted at turn 3 re-prefills about 3,000 tokens, while eviction at turn 12 costs about 12,800. The worst case forces every miss to peak session depth, paying the maximum re-prefill on every request. Real traffic produces a distribution of miss depths, which is why realistic latency stays close to best.</p>
<p>The best-case result is the surprising one. With perfectly cached session context, the L4 handles more than 48 concurrent sessions within a 2-second p99 SLA. The 6 session realistic ceiling is the cost of per-session context uniqueness, the turn histories that cannot be shared. It is not a GPU compute limit.</p>
<p>The worst case grows linearly at about 2.1 seconds per additional concurrent session, reaching 102 seconds at 48c. Throughput saturates at 0.32 req/s from 6 sessions onward. The GPU is fully consumed re-prefilling 12,800 tokens per request, and extra concurrency just lengthens the queue.</p>
<h2 id="why-the-knee-is-where-it-is">Why the knee is where it is</h2>
<p>The L4 has 24 GB of VRAM, but far less than that is available for KV cache. The memory that actually holds KV cache is roughly half of raw VRAM.</p>
<h3 id="where-the-memory-goes">Where the memory goes</h3>
<p>vLLM’s <code>gpu_memory_utilization</code> was set to 0.9 for the fp8 and fp16 experiments, reserving about 21.6 GB. After model weights, CUDA graph capture, activation tensors, and block table overhead, about 13 GB remains for KV cache. The TurboQuant experiment used 0.8 (it needs about 2 GB of extra scratch for torch.inductor autotuning at startup), leaving about 10.6 GB.</p>
<h3 id="kv-cache-per-token">KV cache per token</h3>
<p>Qwen3-4B uses GQA with 36 layers, 8 KV heads, and head_dim 128. The per-token KV cache size depends on precision.</p>
<pre><code>2 (K+V) × 36 layers × 8 KV heads × 128 head_dim × bytes_per_element

FP16/BF16: ... × 2 bytes = 147,456 bytes/token  → ~89K tokens in ~13 GB
FP8:       ... × 1 byte  =  73,728 bytes/token  → ~178K tokens in ~13 GB
TQ4:       ~0.53 B effective (4-bit + quantization metadata)
           =  ~38,700 bytes/token  → ~275K tokens in ~10.6 GB
</code></pre>
<h3 id="the-capacity-arithmetic-with-apc">The capacity arithmetic (with APC)</h3>
<p>With APC, the 10,000-token system prompt is stored once and shared. Only the per-session unique context (about 12,800 tokens at peak depth) needs its own blocks.</p>
<pre><code>FP8 KV
Token budget:     ~178K
Shared prefix:     10K (1×)
Available:        ~168K
Per-session:      ~12.8K
Max sessions:   168K / 12.8K ≈ 13      Observed knee: ~14c

FP16 KV
Token budget:      ~89K
Shared prefix:     10K (1×)
Available:         ~79K
Per-session:      ~12.8K
Max sessions:    79K / 12.8K ≈ 6       Observed knee: ~8c

TQ4 KV (estimated)
Token budget:     ~275K (0.8 util)
Shared prefix:     10K (1×)
Available:        ~265K
Per-session:      ~12.8K
Max sessions:   265K / 12.8K ≈ 21      Predicted knee: ~23c
</code></pre>
<p>The arithmetic predicts the knees within 1 to 2 sessions of the observed values. The slight overshoot (observed 14 sessions against predicted 13) is because sessions are not all at peak depth at once. Earlier turns have smaller contexts, which buys a few extra sessions before capacity runs out.</p>
<p>In this setup, the concurrency cliff is a memory limit. The binding constraint is how many sessions’ KV caches fit in VRAM at once. The best-case bound supports this. With perfect caching, the same GPU handles more than 48 sessions within 2-second p99. Compute, scheduling, and continuous batching also contribute, but memory capacity sets the ceiling.</p>
<h2 id="how-to-find-the-knee-for-your-workload">How to find the knee for your workload</h2>
<p>The knee location depends on three variables. Available KV cache memory is total VRAM minus model weights, CUDA graphs, activations, and fragmentation, typically about half of raw VRAM, and vLLM reports the exact number at startup. Per-session unique context is the total session tokens at peak depth, minus any shared prefix cached by APC. KV precision is the bytes per element, where halving it from fp16 to fp8 to 4-bit roughly doubles token capacity at each step and shifts the knee right.</p>
<p>The estimate is max concurrent sessions ≈ (token capacity − shared prefix) / per-session unique context.</p>
<p>For this setup (Qwen3-4B, L4, 22.8K-token agentic sessions with a 10K shared prefix), the arithmetic predicts about 13 sessions (fp8) and 6 (fp16). The observed knees are about 14 and 8. The arithmetic gives a first-order estimate, and a concurrency sweep gives the precise number. The gap between estimate and observation comes from session depth staggering, block fragmentation, and APC reuse patterns.</p>
<p>Different workloads shift each variable. A single-turn QA workload with 2K tokens per session has a much higher knee. A code review agent with 50K-token inputs has a much lower one. A GPU with more VRAM (A100, H100) raises the budget. The method is the same. Estimate the budget, divide by per-session cost, then verify with a sweep.</p>
<h2 id="what-this-means-for-deployment">What this means for deployment</h2>
<p>Know your KV budget before you set your concurrency limit. Below the knee you get the best throughput with stable latency and effective caching. Above it you get worse throughput, worse latency, and a wasted APC investment.</p>
<p>KV quantization is a direct concurrency multiplier. On this L4, switching from fp16 to fp8 KV cache moves the 2-second p99 SLA ceiling from about 4 to 6 (50 percent more sessions) and the eviction knee from about 8 to 14 (75 percent more sessions). The gain is a direct consequence of halving the bytes per KV element. Quantization buys memory, and memory buys concurrency.</p>
<p>Monitor tail latency, not averages. After the cliff, p50 looks manageable while p99 is catastrophic. The bimodal distribution means some users get sub-second responses while others wait 40-plus seconds, and an average-based dashboard hides it until users complain.</p>
<p>If some of your sessions are latency-tolerant background work, run them off the interactive path. They do not need to compete for cache memory, and keeping them off it frees KV budget for the sessions that need low TTFT.</p>
<p>Several caveats temper the numbers. These experiments use synthetic token content, not real code. The workload has a fixed 12-turn structure, while real agent sessions vary widely in depth. Poisson arrivals do not capture bursty agentic traffic, where agents send follow-up requests immediately. p99 at high concurrency is noisy, since with about 200 requests per run it is only the second-worst request. Chunked prefill, not enabled here, could smooth the knee transition. The numbers are specific to a single L4 with Qwen3-4B. Larger models, multi-GPU setups, and different context lengths shift the absolute numbers while the pattern holds.</p>
<p>KV cache behaves like a systems problem, and the concurrency knee is where that meets a specific GPU, a specific model, and a specific workload shape. The math is simple. The discipline is running it before production tells you the hard way.</p>]]></content:encoded>
  </item>
  <item>
    <title>Your KV cache benchmark is “hi hi hi”</title>
    <link>https://www.gomomento.com/jp/blog/your-kv-cache-benchmark-is-hi/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/your-kv-cache-benchmark-is-hi/</guid>
    <description><![CDATA[<p>Default KV cache benchmarks run on repetitive "hi hi hi" text, which makes compression and transfer look far better than they do on real workloads.</p>]]></description>
    <content:encoded><![CDATA[<p>Before you commit to a KV cache offloading system, you benchmark it to make sure it performs well below your SLA. You see that it has excellent compression and cheap transfers. Seems like an easy win.</p>
<p>But there’s some trouble with what standard KV cache benchmarks run on.</p>
<p>LMCache ships with a <a href="https://docs.lmcache.ai/getting_started/benchmarking.html">long-document benchmark</a> for measuring KV cache offloading performance. Run it without a corpus file and it generates documents like this:</p>
<pre><code class="language-python">warmup_prompts = [
  str(i) + " " + " ".join(["hi"] * args.document_length)
  for i in range(args.num_documents)
]
</code></pre>
<p>A 10,000-token document comes out looking a little underwhelming.</p>
<p><code>0 hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi …</code></p>
<p>Technically speaking, it is the 10K token count you were looking for, but it doesn’t represent a real 10K token workload.</p>
<p>KV cache systems do not run on token count alone. Compression ratios, activation patterns, transfer sizes, and cache behavior all depend on the shape of the input. Two documents of the same length can be two entirely different workloads.</p>
<p>Unfortunately, much of the current KV cache ecosystem is benchmarked on synthetic inputs that look nothing like the workloads people run.</p>
<h2 id="the-benchmark-is-not-representative">The benchmark is not representative</h2>
<p>The default benchmark document contains a numeric identifier and the token “<em>hi</em>” repeated thousands of times.</p>
<p>Transformers do not produce identical activations for repeated tokens. Positional encoding, attention mixing, and residual connections keep every position distinct. To an LLM, <em>distinct</em> and <em>varied</em> are not the same thing. Repeating a single token produces far more regular activation patterns than diverse text does.</p>
<p>Compression improves, transfer sizes shrink, and cache behavior becomes easier to predict with non-varied workloads. The benchmark is measuring <em>something</em>, but it is not measuring a realistic production workload.</p>
<p>Benchmarking KV cache offloading with “hi hi hi” is like benchmarking a database with <code>SELECT 1</code>. The numbers come back fast, but they do not tell you much about real workloads.</p>
<h2 id="the-difference-shows-up-immediately">The difference shows up immediately</h2>
<p>We compared the default benchmark document against a realistic medical document using <a href="https://huggingface.co/Qwen/Qwen3-8B-FP8">Qwen3-8B-FP8</a>. Both ran about 10,000 tokens. The synthetic one carried two unique tokens, a token diversity of 0.02 percent. The medical one carried 1,329, or 13.3 percent. The token count is the same. Everything else is different.</p>
<p>Token diversity affects activation patterns. Activation patterns affect tensor value distributions. Tensor distributions affect compression ratios and transfer sizes. A benchmark built from highly repetitive inputs can diverge sharply from one built on realistic text.</p>
<h2 id="compression-behaves-differently-too">Compression behaves differently too</h2>
<p>We first ran into this while building a Valkey-backed KV cache connector. We followed the LMCache tutorials and used dummy weights, and compression ratios looked excellent. Then we switched to Qwen3-8B-FP8 with real trained weights, and the results were night and day.</p>
<p>Real model weights produce tensor values that behave more like high-entropy floating-point data. General-purpose compression still helps, but the gains are smaller than they appear with dummy weights. Repetitive inputs create more structured activation patterns that compress more effectively, making the benchmark results appear better than they actually are.</p>
<h2 id="so-we-built-a-more-realistic-corpus">So we built a more realistic corpus</h2>
<p>To see how KV cache systems behave under representative inputs, we built a corpus of 30 long-form documents across medical and legal domains. We wanted documents that resemble the structure, formatting, vocabulary, and variability that real systems process.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-24_kv-cache-benchmark/corpus.avif" alt="Example medical and legal document text"></p>
<p>Medical documents averaged 14.2 percent token diversity. Legal documents averaged 9.4 percent. Both are hundreds of times more diverse than the synthetic baseline at 0.02 percent.</p>
<p>The corpus holds 300,000 tokens and was generated with Qwen3-8B-FP8. The corpus and generation scripts are <a href="https://github.com/momentohq/kvcache-corpus">open source</a> for those interested.</p>
<h2 id="benchmarking-the-workload-you-actually-have">Benchmarking the workload you actually have</h2>
<p>While token count is easy to generate, it can also be the least informative. Match the diversity, structure, and vocabulary of the text your system serves, and the compression ratios and transfer sizes start to represent values you can trust.</p>
<p>The input comes first. Before ranking cache connectors, compression schemes, or storage backends, run them on inputs that look like your traffic. Compare them on “hi hi hi” and you are ranking them on a workload nobody runs.</p>
<p>Before you evaluate your next KV cache offloading system, be sure to ask “Are the benchmark documents representative of the workload I actually run?”</p>]]></content:encoded>
  </item>
  <item>
    <title>vLLM's Hash Chain and Why Prefix Caching Is Still Prefix Caching</title>
    <link>https://www.gomomento.com/jp/blog/prefix-caching-is-still-prefix-caching/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/prefix-caching-is-still-prefix-caching/</guid>
    <description><![CDATA[<p>Automatic prefix caching, hash chains, radix trees. The data structures get cleverer, but we haven’t moved past shared prefixes.</p>]]></description>
    <content:encoded><![CDATA[<p>Automatic Prefix Caching sounds like it should solve a bigger problem than plain prefix caching. Requests are hashed, cache entries are discovered automatically, and shared work never has to be tracked by hand. It looks like a system that can find reusable computation wherever it appears.</p>
<p>But it’s still the same type of reuse we’ve always had. Shared prefixes are reusable. Shared content that is not a prefix is not. That rule sets the biggest limit on what today’s inference infrastructure can reuse.</p>
<p>Most of the recent work in KV caching has focused on finding prefixes more efficiently. Hash chains, radix trees, and automatic discovery all improve the mechanics of reuse. But they don’t change what can be reused. The workloads where it succeeds, and where it falls short, show why.</p>
<h2 id="when-prefix-caching-is-enough">When prefix caching is enough</h2>
<p>Reusing the KV cache only pays off when requests share computation, so the question is how much of a real workload today’s infrastructure can reuse.</p>
<p>For many agentic workloads, the answer is often “enough”. Stable system prompts, tool definitions, and conversation scaffolding create long shared prefixes, and inference engines are good at finding and reusing them.</p>
<p>Plenty of workloads share more than just prefixes, though. RAG pipelines are where it breaks down. The system prompt stays fixed while the retrieved documents change from request to request. Two requests might carry the same five documents in a different order. The meaning is almost identical. The token sequence is not, and the cache matches on the token sequence. Same content, different positions, no reuse.</p>
<h2 id="why-prefix-caching-remains-prefix-bound">Why prefix caching remains prefix-bound</h2>
<p>vLLM’s <a href="https://docs.vllm.ai/en/v0.20.1/features/automatic_prefix_caching/">Automatic Prefix Caching</a> uses content hashing to remove the need for explicit prefix tracking. The KV cache is divided into fixed-size blocks, and each block gets a SHA-256 hash. The hash for block N folds in the hash of every preceding block along with the content of the current block. Chained together, every block fingerprints not only its own content but the entire token history before it.</p>
<p>When a request arrives, vLLM hashes each block-sized chunk of input and checks whether a matching block already exists. Matching blocks reuse previously computed KV cache. Missing blocks trigger fresh computation. Lookup is effectively constant-time, eviction is straightforward, and fixed-size blocks map cleanly onto GPU memory.</p>
<p>But because each block hash depends on every block before it, one divergence changes every hash that follows. Take two requests that share the first 3,000 tokens and split at token 3,001. The block holding token 3,001 hashes differently, and so does every block after it. Reuse stops at the point of divergence. The system discovers shared prefixes on its own, and it can’t discover shared content that appears once requests have diverged.</p>
<p>Reuse happens only at block boundaries. If two requests share 1,000 tokens and a block holds 16, vLLM reuses 62 whole blocks, or 992 tokens, and recomputes the remaining 8. For long prefixes that waste is negligible. For short or irregular shared segments it’s more substantial.</p>
<p>There is no matching inside a block, either. Two blocks that share 15 of their 16 tokens still hash to completely different values, so reuse is all-or-nothing at the block level. These are reasonable tradeoffs that keep the implementation simple and fast, but they still leave you with the same limitation: reuse follows exact prefix structure.</p>
<p>A different cache structure might help. <a href="https://docs.sglang.io/">SGLang</a> takes that route. Instead of hashing fixed-size blocks, it keeps cached state in a <a href="https://www.lmsys.org/blog/2024-01-17-sglang/">radix tree indexed by token sequences</a>. When a request arrives, the runtime walks the tree and finds the longest matching cached prefix, and matches can fall on arbitrary token boundaries rather than fixed block ones. That helps workloads with variable-length turns, branching conversations, and irregular prefix lengths.</p>
<p>The radix tree still searches for the longest shared prefix, though. Once two requests diverge, the content they share later in the sequence stays out of reach. SGLang improves how prefixes are discovered, but it does not extend reuse past prefixes.</p>
<h2 id="beyond-prefixes">Beyond prefixes</h2>
<p>Prefix caching tells us that KV reuse clearly works. The more open question now is how much reuse survives divergence. Today’s runtimes are tuned for shared prefixes. The next generation goes after shared segments, cache repair, and the reuse that prefix matching cannot reach.</p>
<p>So much of the current research now focuses on cache repair and segment-level reuse. The goal has shifted from proving that KV reuse is valuable to recovering the work that today’s prefix-based systems still leave behind.</p>]]></content:encoded>
  </item>
  <item>
    <title>Disaggregation makes KV cache a system primitive</title>
    <link>https://www.gomomento.com/jp/blog/disaggregation-makes-kv-cache-a-system-primitive/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/disaggregation-makes-kv-cache-a-system-primitive/</guid>
    <description><![CDATA[<p>Prefill and decode want different hardware. Separate them and the KV cache becomes the state that connects the two, which turns it from an implementation detail into a system design concern.</p>]]></description>
    <content:encoded><![CDATA[<p>Inference is scaling faster than the serving architectures around it. Prefill and decode are often treated as one pipeline, but they are fundamentally different workloads.</p>
<p>Prefill is compute-heavy, with an order of magnitude more arithmetic intensity than decode. Decode is sensitive to memory bandwidth and to latency. Prefill wants high-FLOPS accelerators. Decode wants large, fast memory. Put both on the same GPU and you tune it for one profile while it absorbs the other, and under load prefill interferes with decode. Neither phase gets the hardware it would choose.</p>
<h2 id="the-cache-becomes-the-connection">The cache becomes the connection</h2>
<p>Disaggregation separates these two phases. Prefill nodes do prefill, decode nodes do decode, each on hardware suited to its bottleneck.</p>
<p>Separation removes the interference, but it opens a gap. Inside one accelerator, prefill flows straight into decode and the intermediate state never leaves the chip. Pull the two onto different machines and that state, the KV cache, has to be handed across. Prefill produces it, decode consumes it, and disaggregation turns it into the object that connects them.</p>
<p>On a single node, the KV cache is largely an implementation detail the inference engine manages for you. But once prefill and decode are separate systems, the cache is the connection between them, and every request depends on getting it from one to the other.</p>
<h2 id="what-the-split-asks-of-the-cache">What the split asks of the cache</h2>
<p>Once the cache has to travel between machines, it takes on the requirements of any object moving through a distributed system.</p>
<p>The cache has to move from prefill nodes to decode nodes, which makes transfer latency, serialization format, and network bandwidth first-order concerns. It has to land in the right place, so deciding which decode node receives which cache turns routing into a scheduling problem. It has to expire, which raises the question of who evicts an entry and when, work the engine handled in a colocated system and that now needs coordination. And it has to live somewhere across GPU memory, host memory, NVMe, and remote storage, each tier with its own latency and capacity tradeoffs.</p>
<p>These are distributed systems problems that present themselves whenever you separate storage and compute. What was colocated becomes independently addressable, connected by a transfer layer. The techniques for solving them, placement, routing, eviction, and tiered storage, are well understood. What is new is applying them to the KV cache inside inference serving.</p>
<h2 id="from-implementation-detail-to-system-design">From implementation detail to system design</h2>
<p>The major inference stacks are already built around this split. <a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo</a> describes disaggregated inference as a prefill engine that computes the prefill phase and generates KV cache, hands that cache to a decode engine, and lets the decode engine run the decode phase. AWS is building the split into its infrastructure, with <a href="https://aws.amazon.com/machine-learning/trainium/">Trainium</a> for compute-heavy prefill, and the <a href="https://www.cerebras.ai/blog/cerebras-is-coming-to-aws">Cerebras partnership</a> likely points the same way, since wafer-scale SRAM suits memory-bound decode.</p>
<p>In each of these, the KV cache is the object the tiers hand between them. On a single node it was an optimization you could run, skip, or tune, and the architecture did not care. Disaggregation moves those same problems up a level, from implementation details the engine used to hide to system design concerns the architecture has to own. The cache now has to be transferred, routed, stored, and expired across machines, and the whole system is built around getting it from prefill to decode.</p>
<p>What began as transient state inside a single request becomes the handoff between two systems. Once we have that handoff, cache management becomes a first-class primitive of the architecture.</p>]]></content:encoded>
  </item>
  <item>
    <title>KV Caching Pays Off Under Load</title>
    <link>https://www.gomomento.com/jp/blog/kv-caching-pays-off-under-load/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Tue, 16 Jun 2026 07:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/kv-caching-pays-off-under-load/</guid>
    <description><![CDATA[<p>KV cache starts out as an implementation detail of inference. As inference systems evolve, it is becoming a first-class systems primitive.</p>]]></description>
    <content:encoded><![CDATA[<p>KV caching looks like a bad trade on paper. Memory, complexity, and operational surface area, all spent to shave a few percent off a request.</p>
<p>The benchmarks do not rescue it.</p>
<p>We’ve seen teams leave it at that. KV cache is necessary inside a single forward pass, so you keep it for the life of the request and move on. Keeping it alive beyond that, reused across requests, starts to sound like a serving-layer luxury. You picture the memory it would pin, the eviction logic, the extra moving parts in a stack that is already hard enough to operate. Set that against a few percent of latency and the trade does not look worth making.</p>
<p>Understandably so. Run the numbers on a single request and long-lived KV caching underwhelms. We ran them, and at first it was a very unflattering story. But a single request is the wrong unit to judge this on, and once you measure at production load the economics turn. </p>
<h2 id="the-single-request-savings-are-bounded">The single-request savings are bounded</h2>
<p>In one of our <a href="https://ollama.com/library/qwen3:30b-a3b">Qwen3-30B-A3B</a> runs, a 1K input / 512 output request came in around 135 ms TTFT and about 2.5 seconds of total request latency. TTFT carries scheduling and queueing overhead on top of raw prefill compute, so call the prefill portion roughly 100 ms. Erase it completely, the most a perfect cache hit can do, and you save about 4 percent of the request. Treat that as the ceiling, not the everyday case.</p>
<p>As context grows, so does prefill’s share of the total request latency. At 16K input / 512 output, TTFT was about 769 ms of 3,200 ms total, which puts it near 24 percent. That is a real step up from the 1K case. The input/output ratio affects prefill more than the context length does. KV cache earns the most when a request carries a large input and returns a small output, because prefill is then a bigger share of the bill. In scenarios where you have short input and long output, decode takes over while the cache has little room to help.</p>
<p>On its own, a 4 or 24 percent share looks modest. Latency is measured at a target throughput, GPU capacity is scarce, and as throughput climbs, utilization, queueing, and pipeline stalls increase the cost of redoing prefill. So in production, prefill becomes a capacity and tail-latency problem once thousands of requests compete for the same accelerators.</p>
<p>So the skepticism is fair, for the single request. If the only question is whether one cache hit meaningfully cuts one request’s latency, the answer is often no, and it depends on the input/output ratio and how much of the request prefill actually owns. At that level, KV caching is not an automatic win.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/prefill-vs-decode.avif" alt="Prefill vs decode ratios"></p>
<p>Large-input, small-output workloads are getting more common, not less. Agentic workloads are multi-turn by nature. Context grows as chat history, tool-call results, and retrieval chunks pile up, so each new turn carries a larger input against a small output. Exactly the type of workload where prefill dominates and where reusing the cache has the most to give.</p>
<p>For application teams, reusing the cache shows up as lower TTFT, tighter p95, and lower cost per request. For the platform teams running the GPUs, it shows up as higher utilization and more capacity per dollar. </p>
<h2 id="expensive-to-hold-hard-to-reuse">Expensive to hold, hard to reuse</h2>
<p>That said, the KV cache is not free to keep. Hold it in GPU memory and it competes with active inference for the scarcest space you have. Move it to host memory and it is cheaper but still bounded, with DRAM prices trending the wrong way. Push it out to remote memory or storage and you take on transfer latency, placement problems, and more operational surface. KV caching is a bet. You are spending scarce memory on the wager that future requests reuse the work you are holding.</p>
<p>But that’s only half the problem. Even when you are willing to pay for the memory, the reuse you get back is limited. The production-friendly option today is prefix caching: if a later request begins with the exact same prefix, the engine reuses the KV cache already computed for it. The rule is strict, exact prefix match or nothing. Plenty of real workloads share meaning without sharing a prefix. Reordered retrieval chunks, varying tool results, and shifting user context carry the same semantic content in different positions, and none of it counts as a hit, so hit rates suffer. </p>
<p>It might seem like KV caching has a lot going against it. The single-node latency win is bounded. The memory cost is high. The reuse model is narrow. Evaluate it as an isolated optimization on one node and the honest question is whether the complexity earns its place. In isolation, often it does not. But isolation is the wrong frame because inference is not the system it was when those objections were formed. Each of them was measured against a single node running prefill and decode together, holding a cache that was large and expensive to keep. Two things have shifted since. The first is structural, in where prefill and decode run. The second is economic, in what the cache costs to hold and to move. Each one undercuts a different piece of the case against. </p>
<h2 id="inference-is-becoming-a-distributed-systems-problem">Inference is becoming a distributed systems problem</h2>
<p>Prefill and decode are not the same kind of work. Prefill is compute-heavy. Decode is sensitive to memory bandwidth and to latency. Put both on the same accelerator and you force a compromise on one to serve the other. Split them, and you create a clean boundary between two workloads that want different things. The KV cache, however, has to cross that boundary.</p>
<p>When prefill and decode live on different hardware, the KV cache becomes a first-class distributed systems primitive, something you transfer, place, and manage a lifecycle for. NVIDIA Dynamo and the disaggregated stacks coming out of AWS and Cerebras are building the split into the infrastructure itself, which is what forces developers to think about how the KV cache moves, where it lives, and how long it stays alive.</p>
<h2 id="the-economics-are-starting-to-move">The economics are starting to move</h2>
<p>The second shift is economic. The KV cache itself is getting more efficient to store and to move. A surprising amount of recent model progress is really KV cache innovation, and the last 18 months have been striking. </p>
<p><a href="https://arxiv.org/abs/2405.04434">DeepSeek-V2 and V3</a> introduced <a href="https://mccormickml.com/2025/04/26/inner-workings-of-mla/">Multi-head Latent Attention (MLA)</a>. MLA compresses keys and values into a shared low-rank latent vector before anything gets cached. For V3, that takes the per-token cache from roughly 16,384 scalar dimensions under standard multi-head attention down to 576, a 512-dimensional latent plus 64 dimensions for decoupled RoPE. Against an MHA baseline that is about a 28x reduction. Against the GQA baseline most modern models already use, it is smaller, roughly 4 to 8x depending on group size, and MLA gets there while holding MHA-level quality, which GQA gives up.</p>
<p><a href="https://qwen.ai/blog?id=qwen3.5">Qwen 3.5</a> goes a different way with a <a href="https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/08_deltanet/README.md">Gated DeltaNet hybrid</a>. It replaces 75 percent of its attention layers with Gated DeltaNet linear attention, layers that hold a fixed-size state matrix, 128 by 128 per head, and update it incrementally with each token. The state does not grow with sequence length. Only the remaining 25 percent, full softmax attention with GQA, still needs a traditional KV cache. At long contexts, where the KV cache usually dominates memory, this removes most of the growth. The payoff scales with context: substantial at 256K tokens, modest at 1K, where a fixed-size state costs about what a small KV cache would anyway.</p>
<p><a href="https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/">TurboQuant and PolarQuant</a>, from Google at ICLR 2026, take yet another angle. Instead of changing the attention mechanism, they quantize the KV cache itself to 3 or 4 bits per coordinate with no measurable accuracy loss on standard benchmarks. PolarQuant rotates vectors with a random orthogonal matrix so the coordinates follow a known distribution, then applies an optimal Lloyd-Max scalar quantizer, and QJL adds a 1-bit residual correction. At 4 bits the paper reports up to 8x faster attention on an H100. At 3 bits, roughly 6x memory reduction.</p>
<p>The exact numbers depend on baselines and configurations, but the direction is obvious. MLA shrinks the cache dramatically. Hybrid architectures such as Qwen’s Gated DeltaNet reduce cache growth across much of the network. Quantization approaches such as TurboQuant reduce memory requirements further without changing the model architecture. Different tradeoffs, same trend: the object is getting smaller. </p>
<p>Memory cost is the usual objection to KV caching, but this recent work almost makes it moot. Shrink the cache by 6x to an order of magnitude and the economics look very different. More entries fit in the same budget. Transfers from remote memory, SSD, or another node get faster. There’s a misconception that the cache has to become trivially small. But it only has to get small enough that the economics cross over for the workloads people run in production.</p>
<p>The storage hierarchy is changing as well. The previous thought was if the KV cache is not in GPU memory, it is too slow to matter. That is getting harder to say. Fast interconnects and local NVMe continue to improve. Now the question is whether moving or loading the cache can free the decode GPU from repeated prefill work and keep it pointed at the latency-sensitive part of the pipeline. If a storage-backed cache lowers prefill pressure and keeps accelerator capacity on decode, it can pay off quickly.</p>
<p>The workload is the main success driver. For a given model and context length, the system weighs the time to recompute prefill against the time to move the cache over the network, the time to read it from SSD, the cost of reserving the memory or storage, and the odds the cache gets reused at all. When transfer or load time comes in well under recompute time and reuse is likely enough, the cache earns its place. When it does not, the cache is a cost with no return.</p>
<h2 id="prefix-caching-under-load">Prefix caching under load</h2>
<p>We saw this behavior in an experiment we recently ran. We used Qwen3-1.7B on an L40S, a 10K-token prompt, and the number of shared prefix tokens varied from 0 to 10K across several concurrency levels. As the shared prefix grows, the vLLM prefix cache hit ratio climbs from 0 to 1. Throughput and request latency were monitored at each concurrency level.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_cache_hit_ratio.avif" alt="Plot graph of cache hit ratio"></p>
<p><em>Prefix cache hit ratio grows linearly with shared prefix length, from 0 to 10K tokens against the fixed 10K-token prompt.</em></p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_rps.avif" alt="Plot graph of shared prefix and concurrency"></p>
<p><em>Throughput rises as the shared prefix grows and is sharpest at high concurrency, where skipping repeated prefill lets the system sustain more requests per second.</em></p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_request_latency.avif" alt="Plot graph of request latency"></p>
<p><em>Mean request latency falls as reuse increases, with the high-concurrency settings improving most.</em> </p>
<p>At low concurrency, a higher hit ratio helps, but only a little. At high concurrency, the same increase results in a much larger system effect. Requests per second climb sharply as more of the prompt comes from cache, and mean latency decreases along with it, dropping fastest at the higher concurrency levels. That is the behavior one would expect if KV caching is a systems optimization rather than a single-request latency trick.</p>
<p>The experiment is a small model on a single node, so it does not prove the disaggregated-architecture argument on its own. But it does verify that prefix cache hits remove prefill work from the serving path, and the system-level benefit grows with concurrency. The disaggregation thesis is that this gets stronger when prefill and decode run on separate hardware and the KV cache moves between them as a first-class object.</p>
<h2 id="prefix-caching-already-works-for-the-right-workloads">Prefix caching already works for the right workloads</h2>
<p>Prefix caching generally has a narrow sharing mode. For agentic workflows it fits more naturally than you might expect, though the reason it fits changes by category of context.</p>
<p>System prompts are the easy case. They are stable across requests, they sit at the front of the prompt, and are a textbook prefix hit. An agent making a series of tool calls against the same backend reuses the same 2K to 8K token system prompt on every request. A multi-turn conversation with a fixed system prompt reuses the whole instruction block. A code-generation agent with stable repository context reuses the project description and file summaries. For this category, cross-request caching is straightforward.</p>
<p>Other kinds of context ask for more care. Chat history grows and shifts from turn to turn. Tool-call exemplars get reordered or swapped. Retrieval chunks change with every RAG query. These often share material across requests <em>without sharing an exact prefix</em>, so the effectiveness of the cache comes down to how much of the context is positionally stable (which prefix caching handles), versus variable (which needs something like CacheBlend to unlock). </p>
<h2 id="research-for-a-better-solution">Research for a better solution</h2>
<p>For messier patterns, like RAG with retrieval chunks that vary by query or tool results that differ between calls, two requests can share a great deal of material without sharing the exact same prefix, and classic prefix caching returns a miss in those cases even when most of the computation could have been reused.</p>
<p><a href="https://arxiv.org/abs/2405.16444">CacheBlend</a> is one of the research directions in this area. It is exploring the idea of <em>cache repair</em>, which takes a semantically similar cached entry to what the current request needs, and selectively recomputes only the parts that differ. If repair is cheap enough, individual caches become reusable across more requests and hit rates rise without spending more memory.</p>
<p>This is still in open research, it’s not solved yet. No major inference framework ships chunk-level KV reuse today. The selective recomputation carries its own latency, quality preservation depends on the workload, and the methodology for measuring these tradeoffs is still maturing. But the direction is promising. More flexible cached entries raise the effective hit rate inside the same memory budget.</p>
<p>Prefix caching answers “<em>does caching work</em>?” for a growing number of workloads. The open question is how much of the rest can be brought into the cacheable regime, and cache repair is where we are working that out.</p>
<h2 id="from-per-request-state-to-systems-primitive">From per-request state to systems primitive</h2>
<p>If you evaluate KV caching as an isolated optimization on a single node, it doesn’t make much sense. The memory cost is high and prefix-based reuse is limited.</p>
<p>But the architecture underneath is changing. Disaggregated prefill and decode create the right interface. Better attention mechanisms shrink the object you have to store and move. Faster networks and SSDs reduce transfer costs. Cache repair could push reuse beyond strict prefixes. Scarce GPU capacity makes repeated prefill work harder to justify.</p>
<p>Together, these shifts are turning the KV cache from a temporary intermediate state into an inference systems primitive.</p>]]></content:encoded>
  </item>
  <item>
    <title>Beyond the Goals, Three Ways Momento Scales the Football World Cup in Real Time</title>
    <link>https://www.gomomento.com/jp/blog/beyond-the-goals-three-ways-momento-scales-the-football-world-cup-in-real-time/</link>
  <dc:creator><![CDATA[Lionel Bringuier]]></dc:creator>
    <pubDate>Wed, 03 Jun 2026 16:14:30 GMT</pubDate>
  <category><![CDATA[Media & Entertainment]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/beyond-the-goals-three-ways-momento-scales-the-football-world-cup-in-real-time/</guid>
    <description><![CDATA[<p>When the world&amp;#039;s biggest sporting event kicks off, every millisecond matters. Learn how Momento helps broadcasters and sports platforms deliver faster, smarter, and more secure fan experiences at global scale.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/06/Lionel-Banner-1024x512.png" alt=""></p>
<p><em><a href="https://www.gomomento.com/wp-content/uploads/2026/06/FIFA-infographic.pdf">Don’t have five minutes? This infographic summarizes the key takeaways from this blog.</a></em></p>
<p> The FIFA World Cup 2026 is not just a tournament for Momento. It is a live fire test of what real-time data at global scale actually means.</p>
<p>In stadiums and on sofas, hundreds of millions of fans will see goals, cards, and heartbreak. Behind the scenes, three of Momento’s largest customers will be doing something just as intense: pushing a real-time data platform to the limit across live origination, content protection, and AI-powered personalization.</p>
<p>Beneath these vastly different workloads lies the same fundamental principle that decisions must be made instantly, at global scale, with zero excuses.</p>
<h2 id="what-do-we-mean-by-momento-is-a-real-time-data-platform">What Do We Mean by “Momento is a Real-Time Data Platform”?</h2>
<p>“Data platform” is one of those phrases that can mean anything from a gigantic static data warehouse to a firehose of events in flight. When we say Momento is a real-time data platform, we mean something specific as we combine:</p>
<ul>
<li><strong>A sub-millisecond in-memory data engine</strong>: This is the RAM-cache based data plane that serves reads and writes in less than a millisecond, even under massive load.</li>
<li><strong>An intelligent control plane</strong>: This layer automatically handles sharding, scaling, partitioning, and hot-key management, so app teams do not have to be distributed systems experts</li>
</ul>
<p>In practice, customers can treat Momento like a simple API to store and retrieve state in real time: segments and manifests, concurrency counters, user events, AI embeddings, and more. They describe their data model and policy. Momento makes it fast, durable, and observable.</p>
<p>The FIFA World Cup is a perfect way to show what that actually looks like when the stakes are highest. Think of it as a hat-trick of real-time use cases.</p>
<h2 id="1-a-live-origin-that-just-does-not-flinch">1/ A Live Origin That Just Does Not Flinch</h2>
<p><strong>Who:</strong> A large UK broadcaster holding FIFA rights
<strong>Problem:</strong> Their live origin service (AWS Elemental MediaStore) was deprecated before the World Cup. They needed the same low latency, failover behavior and observability, at World Cup scale, without rewriting their encoder, packager, video player or CDN configurations.</p>
<p><strong>How Momento helps:</strong><a href="https://www.gomomento.com/solutions/momento-media-storage/">Momento Media Storage</a> is their new live origin. It gives them:</p>
<ul>
<li><strong>Predictable, low latency:</strong> Consistent latency for reads and writes at the live edge.</li>
<li><strong>Granular TTL control:</strong> TTL settings on manifests/segments to preserve automatic failover capabilities.</li>
<li><strong>Per-Service Limits and Metrics:</strong> Observability and metrics are built in so high traffic events don’t impact the rest of their 24/7 channels.</li>
</ul>
<p>For viewers, nothing “looks” different. For their ops teams, the origin is now actively developed, supported, faster, and ready for tens of millions of concurrent fans.</p>
<p>🔗 Deep dive <a href="https://www.gomomento.com/blog/a-new-live-streaming-origin-built-for-global-scale/">on How a major UK broadcaster moved its World Cup live origin to Momento ↗</a></p>
<h2 id="2-content-protection-through-concurrency-tracking">2/ Content Protection Through Concurrency Tracking</h2>
<p><strong>Who:</strong> A major US broadcaster holding FIFA rights
<strong>Problem:</strong> Pirates leverage stolen accounts and run illegal restreaming operations on top of the broadcaster’s own CDN. Traditional DRM and short-lived tokens verify a device can decrypt, but are completely blind to whether an account is behaving like a bot farm. Content rights holders are concerned about piracy and are asking broadcasters to implement server-side control over account behavior to shut down the illegal streams at the source.</p>
<p>**How Momento helps:**The broadcaster runs a server-side <a href="https://www.gomomento.com/wp-content/uploads/2025/04/Momento_Concurrency_Overview_OnePager_FINAL_3.17.25.pdf">concurrency service</a> backed by Momento. For each stream, a lightweight verification loop executes three steps:</p>
<ul>
<li><strong>Receive the heartbeat:</strong> The player calls a Momento-powered service with identifiers for the subscriber’s account, their device, the content they want to access, their geography, etc.</li>
<li><strong>Enforce stream limits:</strong> Verify limits on concurrent streams per account and per event.</li>
<li><strong>Deliver instant decisions:</strong> Make allow/deny decisions in single-digit milliseconds, inline with playback.</li>
</ul>
<p>If one account suddenly powers hundreds of devices on the same game, the system can automatically shut it down, protecting revenue, CDN bills, and QoE for legitimate fans.</p>
<p>🔗 Details on the architecture and anti-leeching approach:
<a href="https://www.gomomento.com/blog/stop-cdn-leeching-with-concurrency-tracking/">Stop CDN Leeching with Concurrency Tracking ↗</a></p>
<h2 id="3-ai-powered-personalized-feeds-for-a-sports-app">3/ AI-Powered Personalized Feeds for a Sports App</h2>
<p><strong>Who:</strong> A US-based popular Sports content Super App
<strong>Problem:</strong> Just ahead of the World Cup, this content provider wanted to relaunch their app, with a brand new User Experience. Beyond the traditional “click to watch” from their editorial content, they needed a real-time, personalized feed that mixes editorial pieces, highlights, YouTube clips, and content from popular social networks, tuned to each fan’s behavior as it happens. Think of it as a TikTok “For You Page”, for Sports.</p>
<p>**How Momento helps:**Momento serves as the real-time event collection and embedding layer, making real-time machine learning models visible to the app users at massive scale:</p>
<ul>
<li><strong>Dynamic content ingestion:</strong> New content from newsrooms, creators, social networks, and athletes is ingested and turned into <a href="https://youtu.be/DlKiWkHtsXE?si=LJAcq01lzxwOCJlb">AI embeddings</a>.</li>
<li><strong>Real-time signal streaming:</strong> User signals including emoji reactions, comments, watch time, and scroll depth stream into Momento in real time.</li>
<li><strong>Sub-millisecond personalization:</strong> Recommendation services query Momento’s sub-millisecond data plane to match fresh content to each fan’s evolving interests.</li>
</ul>
<p>The result: A feed that feels instantly relevant and keeps improving as fans interact with their personalized feed, at the scale of millions of concurrent users.</p>
<p>🔗 Video explainer on real-time embeddings with Momento:
<a href="https://youtu.be/DlKiWkHtsXE?si=LJAcq01lzxwOCJlb">Momento AI embeddings &#x26; event collection explainer ↗</a></p>
<h2 id="lets-watch-football-not-infrastructure">Let’s Watch Football, Not Infrastructure</h2>
<p>Three very different workloads, all powered by the same underlying platform: a low latency data plane with an intelligent control layer that simplifies real-time data usage for application teams.</p>
<p>When the first match kicks off, the fans will be watching football, not infrastructure. But inside control rooms, NOCs, and product teams, our customers will know. They will see cleaner dashboards, stronger protections, and faster feedback loops. They will see a real-time data platform doing exactly what it was built to do.</p>
<p>We are excited to be part of their World Cup story. And once the final whistle blows, these capabilities will not disappear. They will become the new baseline for what fans expect from live sports, everywhere.</p>]]></content:encoded>
  </item>
  <item>
    <title>A New Live Streaming Origin Built for Global Scale</title>
    <link>https://www.gomomento.com/jp/blog/a-new-live-streaming-origin-built-for-global-scale/</link>
  <dc:creator><![CDATA[Lionel Bringuier]]></dc:creator>
    <pubDate>Thu, 28 May 2026 20:14:36 GMT</pubDate>
  <category><![CDATA[Media & Entertainment]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/a-new-live-streaming-origin-built-for-global-scale/</guid>
    <description><![CDATA[<p>A major UK broadcaster rebuilt its live streaming origin on Momento ahead of the FIFA World Cup 2026.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/Untitled-design-8-1024x512.png" alt=""></p>
<p>It’s the world’s most watched sport. In the US, it’s soccer; everywhere else, it’s football; and in the UK, it’s nearly a religion. In the run-up to the FIFA World Cup 2026, a large UK-based broadcaster made a big bet: they migrated their live streaming origin off from the deprecated AWS Elemental MediaStore service to Momento, and they did it in time to serve tens of millions of fans.</p>
<p>This wasn’t a simple lift and shift. The broadcaster’s live stack is a mature, battle-tested system that has evolved over years of 24×7 live linear channels, major tournaments and peak news moments. Replacing the core media storage and origin layer under that stack meant Momento had to meet an exacting bar on latency, reliability and operational visibility, while continuing to support an existing fleet of encoders and packagers, CDNs, and control-plane tooling.</p>
<p>This post walks through the existing architecture, adapting Momento as a drop-in replacement for AWS Elemental MediaStore, and then hardening the infrastructure for the World Cup and beyond.</p>
<h2 id="the-starting-point-live-origin-at-scale">The Starting Point: Live Origin at Scale</h2>
<p>The broadcaster runs a large portfolio of 24×7 simulcast channels plus frequent pop-up live events in both HD and UHD, delivered via DASH and HLS. Streams are published into multiple AWS regions for redundancy, and each stream fans out across multiple CDNs.</p>
<p>To give a rough estimate of the data volume, their live origin manages 40+ live 24×7 channels, with some additional seasonal pop-up channels (up to 40 concurrent ones for large sport events). The live channels are encoded in H.264 and HEVC, with typically 8 to 11 video encoding profiles and 4 audio tracks in the ABR ladder. Segments and manifests are pushed into a media storage service deployed in two AWS regions, with cross-AZ replication in each region. On the playback side, CDNs pull from that origin, either directly or via an internal cache concentration layer to distribute traffic across providers and geographies.</p>
<p>At first, the broadcaster evaluated moving the origin to a standard S3 bucket. However, their architecture and operations depended on certain capabilities that would not be guaranteed.</p>
<p>When AWS Elemental MediaStore became deprecated, the broadcaster faced a classic “you have to rebuild the airplane while flying it” challenge: replace a key service in their workflow without rewriting their packagers, CDN configs, video players, or control planes, and without introducing new failure modes at the worst possible time, a year ahead the global football tournament. What were the tenets for the new service?</p>
<ul>
<li><strong>Fast live-edge reads and writes</strong> from London-based clients: tight time-to-first-byte and time-to-last-byte for both PUT (publication from the encoders) and GET (playback).</li>
<li><strong>Transient data policies</strong> to aggressively expire stale manifests, forcing automatic failover to backup origins when the primary stopped publishing.</li>
<li><strong>Per-container request limits</strong> to prevent one high-traffic service from drowning others.</li>
<li><strong>Per-container access policies and CORS</strong> for secure origin access from CDNs and packagers.</li>
<li><strong>Detailed access logging and metrics</strong> for publication latency, error codes, empty object publications, and regional breakdowns.</li>
<li><strong>Lifecycle management</strong> to trim historical content and control storage costs for the content in the hot cache.</li>
<li><strong>Keep the durability of an S3-backed storage</strong> under the hood, but without compromising access latency consistency.</li>
</ul>
<h2 id="design-goal-a-drop-in-mediastore-replacement">Design Goal: A Drop-In MediaStore Replacement</h2>
<p>The joint design goal was straightforward to state but hard to achieve: “just swap the origin to <a href="https://www.gomomento.com/solutions/momento-media-storage/">Momento Media Storage</a> with minimal application changes, while preserving, or improving, the operational semantics we rely on today”.</p>
<p>Concretely, that translated into a few core requirements for Momento:</p>
<ul>
<li><strong>Equivalent HTTP surface area</strong>Keep the same style of HTTP PUT/GET semantics, 404/50x behavior for missing segments, and origin-side access control via headers and tokens.</li>
<li><strong>Performance parity or better from London</strong>For UK-based clients, Momento had to deliver GET and PUT latencies that matched or beat their existing origin across both eu-west-1 (Dublin) and eu-west-2 (London).</li>
<li><strong>Configurable object TTLs to emulate transient data policies</strong>Instead of path-based lifecycle rules on containers, the broadcaster wanted fine-grained TTL control per object class (e.g., manifests vs segments) to preserve their model where a stale manifest can trigger failover.</li>
<li><strong>Operational observability that matched their current dashboards</strong>Publication latency, error codes, throttling, per-path metrics, and near-real-time access logs had to remain available for the broadcaster’s existing monitoring and alerting workflows.</li>
<li><strong>Sensible multi-tenant safety rails</strong>Per-service request limits and regional SLAs needed to be enforced in a way that matched their mental model from the previous platform.</li>
</ul>
<h2 id="today-we-are-ready-for-kick-off">Today: We Are Ready for Kick-Off</h2>
<p>The journey from early performance tests to full production lasted almost a year. During that time, the broadcaster and Momento ran a substantial battery of tests:</p>
<ul>
<li><strong>Distributed publication and playback</strong> across dozens of channels in parallel.</li>
<li><strong>Comparative latency benchmarks</strong> from London-based clients to both eu-west-1 and eu-west-2, under varying bitrates and ladders.</li>
<li><strong>Load and failover drills</strong> to confirm that short manifest TTLs and 404 semantics still triggered the right automatic reactions in their CDN and player stack.</li>
<li><strong>SDK vs native HTTP tests</strong> to iron out any client-side inefficiencies and eliminate measurement artifacts.</li>
<li><strong>Reproducibility and automation,</strong> with a single orchestration layer for the whole video stack.</li>
<li><strong>Operational observability</strong> at every step of the workflow, that slots into their existing CloudWatch-based dashboards and alerting.</li>
</ul>
<p>Most importantly, they are now running on an origin layer that is actively developed, not deprecated, and that can evolve with their roadmap and future needs.</p>
<p>For now the focus is simple: when the referee blows the whistle to start the first World Cup match, tens of millions of fans across the UK and beyond will be watching through a new origin, built on Momento, and they won’t even notice the difference. And that’s exactly how it should be.</p>]]></content:encoded>
  </item>
  <item>
    <title>Introducing valkey-lab: Stop Guessing When Your Cache Hits Its Limit</title>
    <link>https://www.gomomento.com/jp/blog/introducing-valkey-lab-stop-guessing-when-your-cache-hits-its-limit/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Tue, 26 May 2026 20:14:51 GMT</pubDate>
  <category><![CDATA[Valkey]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/introducing-valkey-lab-stop-guessing-when-your-cache-hits-its-limit/</guid>
    <description><![CDATA[<p>Benchmarking a cache should be about finding how much load your system can sustain before latency SLOs break. Introducing valkey-lab.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/valkey-lab-1024x566.jpg" alt=""></p>
<p>Pop quiz: how many requests per second can your cache take before it stops meeting your latency SLO?</p>
<p>Chances are good you don’t know the answer, and that’s not a knock on you. It’s a genuinely hard number to come by. The standard tool for this is <a href="https://valkey.io/topics/benchmark/">valkey-benchmark</a>, and it’s great at exactly one thing: you point it at a server, you hammer it with some commands at full speed, and it prints a throughput number at the end. That number tells you the box is alive and roughly how fast it goes flat out.</p>
<p>From a production standpoint, that’s not as useful as it sounds.</p>
<p>How does the p999 hold up under an 80:20 read/write mix when half the requests land on hot keys? What does the tail look like at the rate you actually plan to run? How much headroom do you have before the SLO breaks? Was that latency spike at 10:32 a fluke or the ceiling? A single summary number printed after a sixty-second run can’t answer any of those, because it threw away the useful bits on the way to computing the average.</p>
<p><a href="https://github.com/cachecannon/cachecannon/blob/main/VALKEY-LAB.md">valkey-lab</a> was built to answer these questions.</p>
<p>It’s a high-performance Valkey and Redis benchmark that uses <code>io_uring</code> for kernel-bypassed I/O, per-connection pipelining, and multi-threaded workers. The defaults are deliberately familiar. Run it without any arguments:</p>
<p>📄</p>
<pre><code>valkey-lab

</code></pre>
<p>and you get a sixty-second run against <code>localhost:6379</code> with an 80:20 GET/SET ratio and a million keys. Same shape as the tool you already know, same short flags (<code>-h</code>, <code>-p</code>, <code>-c</code>, <code>-P</code>, <code>-r</code>). The interesting part starts once you begin asking harder questions.</p>
<h2 id="saturation-search-the-headroom-number-found-for-you"><strong>Saturation search: the headroom number, found for you</strong></h2>
<p>Here’s how you find your headroom number today, by hand. You run a benchmark at some rate, read the p999, decide it looks healthy, bump the rate, run it again, read it again. You do this five or ten times, squinting at each result, trying to find the rate where the tail skyrockets. Somewhere in that loop you lose track of which run had which config. Eventually you settle on a number you’re “pretty sure” is right and plan capacity around it. valkey-lab does that whole search for you with one command.</p>
<p>📄</p>
<pre><code>valkey-lab saturate --slo-p999 1ms -c 16 -P 32

</code></pre>
<p>A synthetic benchmark might say a cache can handle 2M requests per second. But when you add a realistic read/write mix, hot keys, and a warm cache, your p999 suddenly crosses your SLO at 1.2M. Technically speaking, the server is still processing 2M requests per second, but the usable ceiling is significantly lower.</p>
<p><code>saturate</code> starts issuing requests at whatever is provided in <code>--start-rate</code> (or 1000 if not provided) and multiplies the request rate by the <code>--step</code> factor on every step. The default step is <code>1.05</code>, so the load compounds over time. Each step holds its rate for a sample window, measures the full percentile spread, and checks it against your SLO. The moment a percentile crosses the line, the ramp stops and reports the last rate that held.</p>
<p>When a step fails, valkey-lab tells you <em>how</em> it failed, either throughput-limited or latency-exceeded, which helps you tune your clusters more accurately.</p>
<p>Throughput-limited means the server couldn’t generate the requested rate at all. It topped out below the target. That’s a capacity problem: you need more CPU, more shards, or a different topology.</p>
<p>Latency-exceeded means the server kept up with the rate, but the tail blew past the SLO. The server can sustain the requested rate, but something in the path is introducing tail spikes under load. Could be a hot key, a GC pause, a scheduler stall, network jitter. You fix that by chasing the spike, and adding hardware won’t help.</p>
<p>So based on your failure, your mitigation strategy varies wildly. And it would be impossible to know which one to pursue if all you had was the throughput number.</p>
<h2 id="averages-hide-the-interesting-failures"><strong>Averages hide the interesting failures</strong></h2>
<p>The next problem surfaces when the benchmark completes. Summary statistics hide the behavior you’re usually trying to find.  If your p999 was 312µs for fifty-nine seconds and 4.2 ms for one second, the run-level p999 still looks fine. The spike is the important part that you need to focus on.</p>
<p>valkey-lab streams one row per second with the full latency spread:</p>
<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/valkey-lab-1-1.jpg" alt=""></p>
<p>Every major percentile from p50 to p99.99 plus the max, the error count, and the cache hit rate, all per second. A spike that lasts one second appears as one row with a tall tail, a vast improvement over the executive summary at the end of a run. When you need it machine-readable instead, –output json gives you newline-delimited JSON you can pipe straight into something else, and –output quiet collapses the whole run to a single summary line. </p>
<h2 id="make-the-benchmark-look-like-your-workload"><strong>Make the benchmark look like your workload</strong></h2>
<p>There’s an important gotcha with the saturation number, or any benchmark number. A ceiling is only as good as the load that produced it, and the default load most tools run is unrealistic.</p>
<p>Think about what a stock benchmark actually does. It sends all reads, or close to it, because a 100% GET run posts the biggest number (or it’s the easiest to simulate). It picks keys uniformly at random, so every key is equally cold and nothing is ever hot. It runs flat out, measuring throughput at saturation. And it normally starts against an empty cache. Now think about your production traffic. It’s a read-write mix. It has hot keys, with a small fraction of the keyspace taking most of the requests. And the cache is warm. Every one of those differences takes away from the realism of the benchmark run.</p>
<p>valkey-lab addresses each one of these gaps. Set the real read-write split with -r so you’re measuring the write path your cache actually carries. Turn on –distribution zipf so a small fraction of keys receives most of the traffic, like production systems often do. Uniform access patterns avoid contention and hide the behavior of your actual hot paths. </p>
<p>Pin the load with –rate-limit to track latency at the rate you plan to run. And warm the cache with –prefill, or model a read-through cache that fills on miss with –backfill, so a GET benchmark measures hits the way production would.</p>
<p>📄</p>
<pre><code>valkey-lab --prefill -r 100:0 --distribution zipf -c 16 -P 32

</code></pre>
<p>Stack those and the ceiling you measure is a ceiling that meaningfully tracks production. There’s more depth when you need it, warmup tuning, RESP3, pinning workers to cores with –cpu-list, TLS, full TOML configs, but the move that matters is making the four big assumptions match your reality before you trust the number.</p>
<h2 id="getting-the-important-data-from-a-run"><strong>Getting the important data from a run</strong></h2>
<p>Now that we have realistic benchmark data, we have to make sure it’s useful after the run ends.</p>
<p><code>--parquet results.parquet</code> saves the full dataset to disk. It stores the full metric set per snapshot: the counters, the gauges, and the latency distributions as actual nanosecond histograms. Combine this with the visualization functionality in valkey-lab, and you have a rich experience that lets you dig into every tiny detail.</p>
<p>📄</p>
<pre><code>valkey-lab --parquet results.parquet
valkey-lab view results.parquet

</code></pre>
<p><code>view</code> opens an interactive dashboard against the file, with a synchronized time axis you can zoom and pan through dimensions like throughput, hit rate, error rate, and latency split out by GET, SET, and combined, all on a log scale. Scrub to the exact second p999 jumped and read every other metric in that same window. </p>
<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/valkey-lab-2-1024x508.jpg" alt=""></p>
<p>One use case for this is regression testing. Because every run is a Parquet file with the same schema, runs are directly comparable to each other. Benchmark before a Valkey upgrade and after, and the question “<em>did this move my tail latency</em>” is easily answered with a diff. The viewer is one way to read these files, but using your own queries is another easy way to act on changes in performance. <a href="https://duckdb.org/">DuckDB</a>, <a href="https://pandas.pydata.org/">pandas</a>, and <a href="https://pola.rs/">Polars</a> all read Parquet directly, so a few lines of SQL across a directory of runs is a regression suite for cache performance. Point DuckDB at a folder of recorded runs and let it compute peak throughput per file:</p>
<p>📄</p>
<pre><code>SELECT
  filename,
  max(responses_received) AS total_responses,
  max(request_errors)     AS errors
FROM read_parquet('runs/*.parquet', filename = true)
GROUP BY filename
ORDER BY filename;
</code></pre>
<p>That is a before-and-after table for every benchmark you have ever saved, built from data you already recorded. </p>
<p>Another use case for the Parquet output is root cause analysis. A spike on the latency chart tells you when something went wrong, not why. Point <code>view</code> at a <a href="https://github.com/iopsystems/rezolus">Rezolus</a> capture from the server or the client and it overlays system telemetry, CPU utilization, network, scheduler behavior, aligned to the same benchmark timeline. When a p999 spike lines up exactly with a scheduler stall or a network hiccup on the axis above it, you have your answer as simple as that.</p>
<h2 id="stop-guessing">Stop guessing</h2>
<p>Back to the pop quiz. The reason it’s so hard to answer is that traditionally the tool you use to measure max RPS reports a summary and throws the important bits away. valkey-lab changes the approach. It remembers the mix, the hot keys, the per-second tail, and records your runs so you can come back to them. The headroom number that used to take an afternoon of manual ramping is now a single command, and it comes with the failure mode attached so you know what to do about it.</p>
<p>valkey-lab is built on top of <a href="https://github.com/cachecannon/cachecannon">cachecannon</a>, inheriting its workload generation, saturation search, telemetry collection, and analysis capabilities. It needs Linux for io_uring (kernel 6.0+) and builds with Rust, under your choice of Apache-2.0 or MIT. Here is the whole getting-started path:</p>
<p>📄</p>
<pre><code>cargo install --path . --bin valkey-lab
valkey-lab saturate --slo-p999 1ms
</code></pre>
<p>Run that against a Valkey server and see what number comes back. Stop asking “<em>how fast can my cache go</em>” and start asking “<em>how fast can it go before my production workload breaks?</em>” That’s the number you capacity-plan around if you want predictable systems at 3 AM.</p>]]></content:encoded>
  </item>
  <item>
    <title>Why Snap Was Willing to Fork, and Why They Still Came Back</title>
    <link>https://www.gomomento.com/jp/blog/why-snap-was-willing-to-fork-and-why-they-still-came-back/</link>
  <dc:creator><![CDATA[Allen Helton]]></dc:creator>
    <pubDate>Thu, 21 May 2026 19:36:05 GMT</pubDate>
  <category><![CDATA[Caching]]></category>
  <category><![CDATA[Valkey]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/why-snap-was-willing-to-fork-and-why-they-still-came-back/</guid>
    <description><![CDATA[<p>Snap ran 100% of their caching infrastructure on KeyDB, a Redis fork they acquired in 2022. Two years later, they moved to Valkey. Here&amp;#039;s why.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/fork-hero-1024x683.jpg" alt=""></p>
<p>I have no intention of ever forking a database. The amount of bravery and engineering mastery that goes into it scares me to no end. But Snap did. They committed to it so hard that they acquired the company building it, open sourced the entire commercial codebase, and ran 100% of their caching infrastructure on it for years. <a href="https://docs.keydb.dev/">KeyDB</a> powered Snapchat at a scale most companies can only dream of.</p>
<p>And then they migrated to Valkey anyway.</p>
<p>At <a href="https://www.unlockedconf.io/san-jose-replays">Unlocked San Jose</a>, Ovais Khan, Principal Software Engineer at Snap, walked through that migration. As interesting as it was to hear <em>how</em> they did it, it was all the more interesting to hear <em>why.</em> Why it happened, why it wasn’t worth staying on the fork, and why when they came back, they came back to Valkey. </p>
<h2 id="the-case-for-forking-in-2019"><strong>The case for forking in 2019</strong></h2>
<p>KeyDB started in 2019 as a project by John Sully and Ben Schermel at EQ Alpha Technology. The premise was simple. Redis ran a single-threaded event loop. Modern servers had 32, 64, 96 cores. To get peak throughput out of a single machine, you had to run a cluster of Redis nodes on it. That was wasteful, and Salvatore Sanfilippo, the creator of Redis, was on record <a href="https://antirez.com/news/126">arguing against changing it</a>: “<em>I/O threading is not going to happen in Redis AFAIK, because after much consideration I think it’s a lot of complexity without a good reason.</em>” Simplicity of the codebase was a value he was actively protecting. </p>
<p>KeyDB took the other side of that bet. It added real multithreading, with per-thread event loops and lock-based synchronization on shared state. It also added active-active replication and FLASH storage for cost-efficient large datasets. On the same hardware, it could move several times the operations per second that Redis could.</p>
<p>This is the textbook case for forking. The upstream project had made a deliberate architectural choice. That choice was the right one for them and the wrong one for a certain kind of user (Snap) who needed to push a single node harder. A fork was the only way forward.</p>
<p>By 2021, Snap was running KeyDB across enough of their caching infrastructure to want a permanent stake in it. They <a href="https://docs.keydb.dev/news/2022/05/12/keydb-joins-snap/">acquired the team in May 2022</a> and brought the formerly commercial KeyDB Pro features into the open source codebase under BSD-3. For about two years after that, all of Snap was running on KeyDB.</p>
<h2 id="what-forking-buys-you"><strong>What forking buys you</strong></h2>
<p>The benefits of forking are easy to articulate when you ship. Snap got features that were important for their specific operating model:</p>
<ul>
<li>Multithreaded command execution, which let them get more out of every node</li>
<li>Zone-aware read routing, which kept cross-AZ traffic down and cut data transfer costs considerably</li>
<li>Forkless background saves, which made snapshots predictable at high memory</li>
<li>Same-zone replica behavior that reduced timeout blast radius during upgrades</li>
</ul>
<p>These features weren’t going to make it into Redis on Snap’s timeline. The fork gave them room to build it as soon as they were ready.</p>
<p>As far as forking goes, that’s usually the part written in blog posts and talked about on the conference loop. You wanted a feature, the upstream said no, you built it yourself, and now it works. Forking feels like freedom.</p>
<h2 id="what-forking-costs-you"><strong>What forking costs you</strong></h2>
<p>Every change to upstream Redis after the fork point became a decision. Does it get ported over? Rewritten? Skipped? There’s a long tail at the end of whatever decision was made. Porting means you carry merge conflicts forever. Rewriting means you have two implementations of the same idea drifting apart. Skipping means your fork stops being a superset of upstream and starts being something else.</p>
<p>Ovais addressed this specifically in his talk. Snap could not easily move from KeyDB’s Redis 6.2 base to Redis 7.2. The cost of staying current with upstream had become high enough that they were stuck on a flavor of 6.2 while everyone else moved on. That meant they were also stuck without features the broader community had built on top of 7.2.</p>
<p>The same goes for the ecosystem. Every client library, operator, monitoring tool, and benchmark gets tested against upstream first. Your fork either matches upstream behavior closely enough that those tools just work, or it doesn’t, and you start maintaining your own.</p>
<p>While forking might have started off feeling like an accelerator, it quickly became a drag.</p>
<h2 id="the-redis-license-change"><strong>The Redis license change</strong></h2>
<p>In March 2024, Redis Ltd. changed the Redis license <a href="https://redis.io/blog/redis-adopts-dual-source-available-licensing/">from BSD-3 to a dual SSPL and RSALv2 model</a>. Neither license is OSI-approved. For any company offering Redis as a managed service, this was an immediate problem. AWS, Google Cloud, Oracle, and Ericsson responded by forking the last BSD release, Redis 7.2.4, and donating it to the Linux Foundation. Eight days after the license change, Valkey existed.</p>
<p>Up until then, the case for staying on KeyDB was obvious. The KeyDB team was inside Snap. The codebase was theirs. The performance was what they needed. </p>
<p>But Valkey made them pause. The project had open governance under the Linux Foundation, with a Technical Steering Committee across multiple companies and no single controlling vendor. It was BSD-licensed and would stay that way. Its roadmap included the things Snap had previously forked to get: <a href="https://valkey.io/blog/unlock-one-million-rps/">I/O threading</a>, <a href="https://github.com/valkey-io/valkey/issues/2083">dual-channel replication</a>, and a path toward features Snap wanted. And every major cloud provider was committing serious engineering effort to it.</p>
<p>The KeyDB story also got more complicated from the inside. In January 2025, John Sully, KeyDB’s original creator, <a href="https://github.com/Snapchat/KeyDB/issues/895">left Snap</a>. His parting note on the KeyDB repository said it plainly:</p>
<p>“When we made KeyDB we wanted to prove that caches should have great performance and I think we succeeded. Now there are many options, including Valkey which is fully open source and based on my testing has matched KeyDB’s performance. I’m not sure what Snap will do with the project, but I think that development effort should move to Valkey moving forward as they have clear momentum and are the most up to date.”</p>
<p>When the person who started the fork tells you the fork is done, the fork is done.</p>
<h2 id="the-secret-migration-back"><strong>The secret migration back</strong></h2>
<p>Snap runs caching at a scale where you can’t just swap a binary. The migration had to be invisible to application teams, comparable in cost, and safe across radically different workload types. Ovais walked through the major decisions that made their migration as easy as possible.</p>
<h3 id="abstraction-layers-are-key-to-managing-workloads-at-scale"><strong>Abstraction layers are key to managing workloads at scale</strong></h3>
<p>Snap had built a storage abstraction with a RESP proxy in front of every cluster. Applications never talked to KeyDB directly. They talked to the proxy, which spoke Redis wire protocol back to whatever was running behind it. That layer of indirection made this migration possible. Without it, every application team at Snap would have needed to know about the change. With it, nobody had to.</p>
<p>These layers let them migrate around 30 caches per week. By the time Ovais gave this talk, 70 to 80 percent of workloads were on Valkey.</p>
<h3 id="do-a-gap-analysis-before-changing-any-code"><strong>Do a gap analysis before changing any code</strong></h3>
<p>Snap did a feature-by-feature comparison between KeyDB and Valkey before touching anything in production. KeyDB’s multithreading and Valkey’s I/O threading work differently, so they benchmarked carefully to confirm comparable throughput. </p>
<p>Some KeyDB features were blockers and had to be ported to Valkey. Zone awareness was the first one Snap contributed. Replica MOVED behavior during upgrades was another. CPU throttling at high utilization was a third. </p>
<p>A hidden gap that wasn’t found until much later was with <code>MGET</code>. KeyDB supported it across slots, but Valkey does not. So after moving to Valkey, Snap had issues with command parsing pressure in large batching workloads. They quickly ported cross-slot <code>MGET</code> to their internal build, and are working with the core maintainers to get it added upstream.</p>
<h3 id="pick-a-stable-version-for-a-base-not-a-new-one"><strong>Pick a stable version for a base, not a new one</strong></h3>
<p>Snap started on Valkey 8.2 RC, ported the features they needed, and immediately ran into crashes at 9 to 10k QPS. The root cause was new TLS offloading work. They rolled back to 8.0.2, ported the necessary fixes onto that, and benchmarked from there. New releases need a baking period, and a migration is the wrong time to find out.</p>
<h3 id="categorize-and-prioritize-your-workloads"><strong>Categorize and prioritize your workloads</strong></h3>
<p>Snap divided their caches into three categories: CPU-bound, high-memory, and high-write-rate. Each category needed different validation. CPU-bound workloads were primarily a throughput question. High-memory workloads were really about replication buffer behavior during full syncs, because if the buffer fills before a snapshot completes, you enter a sync loop that never finishes. High-write workloads required tuning replica buffer sizes and primary write throttling, because Valkey’s dual-channel replication puts buffers on replicas rather than primaries. Inside each category, they went lowest-criticality first, highest-criticality last.</p>
<h2 id="lessons-from-going-full-circle"><strong>Lessons from going full circle</strong></h2>
<p>The fork was the right call in 2019. Redis was not going to go multithreaded, and the workloads Snap was running needed it. KeyDB was a solid piece of engineering that pushed the ceiling on what a single Redis-compatible node could do.</p>
<p>The migration back was the right call in 2025 because the conditions that justified the fork had changed. The upstream that resisted features they needed was no longer the upstream they cared about. Valkey’s governance was open. Its roadmap included the work Snap had previously done alone. And every additional year on a Redis 6.2 build was another year of compounding distance from where the ecosystem was going.</p>
<p>Forks are leverage. They are also debt. Be honest with yourself about which one you are accumulating at any given moment. Snap was. They forked when forking gave them speed, and they came back when the fork started to cost more than it earned.</p>
<p>I don’t want you to take away from this that forking is bad. Sometimes it’s the right thing to do. The decision to fork is not permanent, and treating it like it is permanent is how you end up running a five-year-old codebase while your competitors are shipping on a roadmap you helped fund.</p>
<p>When the world moves, move with it.</p>
<p>Happy coding!</p>]]></content:encoded>
  </item>
  <item>
    <title>Why Large Payloads Break Caches at Scale</title>
    <link>https://www.gomomento.com/jp/blog/why-large-payloads-break-caches-at-scale/</link>
  <dc:creator><![CDATA[Allen Helton]]></dc:creator>
    <pubDate>Thu, 21 May 2026 16:52:11 GMT</pubDate>
  <category><![CDATA[Scale]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/why-large-payloads-break-caches-at-scale/</guid>
    <description><![CDATA[<p>Valkey degradation at scale rarely traces back to a single oversized object. It traces back to traffic shape, event-loop pressure, and the absence of guardrails that respond to live system state. Lessons from Apple and Snap at Unlocked San Jose.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/large-payloads-hero-1024x563.jpg" alt=""></p>
<p>If you’re running Valkey in production, you’ve probably configured a payload limit somewhere and assumed it protects the cache from oversized objects. But cache failures at scale don’t happen because of a single giant payload. They come from thousands of requests arriving in the wrong shape at the wrong time. </p>
<p>For example, <code>ping</code> latency on a system run by Apple jumped from 300ms to over a second during a burst of large payloads. Input throughput on that instance climbed from 7.5 MB/s to 75 MB/s, and output from 29 MB/s to 185 MB/s. No single item was anywhere close to the default 512MB limit. But with a surge in medium-sized items, things started to crumble because the system’s request processing path was becoming saturated.</p>
<p>Valkey 9.0’s copy avoidance update addressed one failure mode on the read path. Cumulative payload volume, command shape, and write-path allocation pressure are separate sources of pressure that can still degrade cluster performance.</p>
<p>At <a href="https://unlockedconf.io/san-jose-replays">Unlocked</a>, Yiwen Zhang from Apple and Ovais Khan from Snap described production systems where payload size created pressure in different parts of their infrastructure. Both were describing things they had to build their way out of. Here’s what they shared, and where the guardrail logic actually needs to live.</p>
<h2 id="the-event-loop-bottleneck"><strong>The event loop bottleneck</strong></h2>
<p>Everything in Valkey runs through a single-threaded main event loop: reads, writes, command parsing, and reply construction all compete for execution time. To understand why large payloads cause the problems they do, you have to start here.</p>
<p>Yiwen’s framing was that Valkey latency is a result of how long the event loop stays busy and how long it gets blocked. When large payloads enter the picture, they add pressure to the event loop and reduce the time available for other work.</p>
<p>On the read path, the expensive operation for a large <code>GET</code> is reply construction. Before Valkey 9.0, that meant a full memory copy of the value into a reply buffer on the main thread before any I/O thread could take over. A 5MB <code>GET</code> meant a 5MB <code>memcpy</code> blocking the loop, which causes every other client to wait.</p>
<p>Valkey 9.0’s copy avoidance helps with this for several use cases (normal client, RAW encoding, object size above the 16KB or 64KB limit). The main thread writes a reference instead of copying the value, and <a href="https://www.gomomento.com/blog/large-objects-in-valkey-9-0/">I/O threads handle the transfer</a>. This solves the most visible noisy-neighbor failure, where a handful of large GETs would block small ones on the main thread.</p>
<p>Valkey gives operators the ability to cap payload size for writes via <code>proto-max-bulk-len</code>. A large <code>GET</code> has no corresponding limit. A 10MB value (like a <code>list</code> that has grown over time) can be returned and contribute to event-loop pressure regardless of what write-side limits are configured. </p>
<p>Event-loop pressure can build gradually from many requests or spike because of a single outlier request. Enough large <code>GET</code>s arriving close together can cause serialization time to dominate the event loop. No individual request needs to be anywhere near the configured limit for the system to degrade.</p>
<h2 id="large-sets-have-a-copy-problem"><strong>Large SETs have a copy problem</strong></h2>
<p>In theory, Valkey can skip the allocation and copy on the way in. For payloads at or above 32KB, it can reuse the query buffer directly as the stored value, but the buffer must be aligned at offset zero, and it must contain exactly that value with nothing else. In pipelined workloads, the buffer will always have something else in it, resulting in most large <code>SET</code>s producing a full allocation and <code>memcpy</code> in the parse phase.</p>
<p>Valkey 8.0’s I/O threading helps with this problem. The parse-phase allocation and copy now run on I/O threads instead of the main thread. Yiwen’s measurement on 256KB SETs (128 clients, pipeline depth 4) showed event-loop time per cycle dropping from ~171 µs with one I/O thread to ~93 µs with four. Throughput stayed flat around 13.3k RPS, and p99 improved roughly 10% (67ms to 60ms). The main thread got its time back even with the copy still occurring.</p>
<h2 id="shape-degrades-before-size-does"><strong>Shape degrades before size does</strong></h2>
<p>Ovais described large batch <code>MGET</code> commands creating regressions at Snap, caused by how the commands interacted with Valkey’s cluster architecture regardless of individual value size.</p>
<p><a href="https://valkey.io/commands/mget/">Valkey’s MGET</a> is slot-based. Each key maps to a hash slot, and in a cluster, slots live on specific nodes. A multi-key <code>MGET</code> that spans multiple slots cannot be served by a single node and returns a <code>CROSSSLOT</code> error. Clients have to crack the batch into per-slot sub-batches and reassemble the result.</p>
<p>KeyDB, <a href="https://github.com/snapchat/keydb">a Redis fork backed by Snap</a>, had a proprietary capability called Cross-Slot MGET: a single command could query all the data hosted on a given node regardless of which slots those keys belonged to. Some of Snap’s workloads relied on this for throughput. When they ported to Valkey, those workloads regressed. Snap ported Cross-Slot MGET to Valkey internally to restore parity and are <a href="https://github.com/valkey-io/valkey/pull/1986">working with upstream maintainers to bring it to the project</a>.</p>
<p>The shape of a command affects system pressure in different ways than payload byte count. How many keys it touches, how those keys are distributed across slots, and how the cluster has to coordinate the response, all affect performance in different ways. At the scale Snap operates, serving hundreds of millions of commands per second, that distinction adds up quickly.</p>
<p>A <a href="https://www.gomomento.com/blog/why-large-cache-systems-need-routing-layers/">RESP proxy in front of the cluster</a> introduces an additional pressure point. One slow command can hold up others behind it in the pipeline. Snap mitigated head-of-line blocking by increasing connection count and limiting pipelining for critical use cases. The root issue is command shape. Large batch commands occupy proxy pipelines longer than smaller individual requests.</p>
<p>Even if you understand these pressure points, they are still hard to identify in production because existing metrics don’t capture how workloads change over time. </p>
<h2 id="payload-drift-is-invisible"><strong>Payload drift is invisible</strong></h2>
<p>Valkey provides solid system-level metrics: bytes per second, operations per second, event-loop health, SLOWLOG, large request and large reply diagnostics. But continuous visibility into payload size distribution is missing. Throughput at 100MB/s looks identical whether that’s 100 operations at 1MB each or 20 operations at 5MB each. However, they have very different costs to the event loop.</p>
<p>Yiwen proposed adding two payload size bucketing metrics (<code>request_payload_bytes_bucket</code> and <code>reply_payload_bytes_bucket</code>) to Valkey’s existing metrics. Bucketing requests and replies by size ranges gives operators the ability to detect when traffic shape shifts. Median payload sizes can slowly increase across a workload over weeks with no alert firing.</p>
<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/large_payloads_1-1024x529.png" alt=""></p>
<p>Runtime guardrails respond to distributions and trends, while static limits react to absolute values at the moment of ingestion. Avoiding payload drift requires both runtime guardrails and static limits.</p>
<h2 id="put-guardrails-above-the-engine"><strong>Put guardrails above the engine</strong></h2>
<p>In Snap’s scenario, applications don’t connect directly to Valkey. They connect through a storage abstraction layer that connects to Valkey using a RESP proxy. This let them switch from KeyDB to Valkey without application teams being aware of the change. This architecture has allowed them to customize the system’s behavior, including partial <code>MGET</code> failure handling and zone-aware routing, without modifying application code.</p>
<p>Snap also added runtime guardrails inside the engine itself. They ported CPU throttling into their Valkey deployment so that when CPU utilization crosses 95%, write requests are throttled to preserve capacity for administrative commands. Protection activates based on what the system is experiencing. Something a byte-count threshold couldn’t do.</p>
<p>The proxy layer gave them a place to absorb large-payload problems like custom <code>MGET</code> handling, partial failure semantics, and connection count tuning to reduce head-of-line blocking. By moving these concerns into infrastructure, application teams no longer needed to own or implement them individually. Large-payload problems became contained to the infrastructure layer instead of every application’s codebase. </p>
<p>Ovais stated that strong abstractions turn risky migrations into operational exercises. The Snap migration peaked at around 30 caches per week through fully hands-off tooling. By the time of his talk, 70 to 80% of roughly 350 caches had moved, with application teams largely unaware it was happening. That’s what becomes possible when guardrails live above the engine.</p>
<h2 id="prioritize-observability"><strong>Prioritize observability</strong></h2>
<p>The most practical step right now is adding payload size visibility to your cache observability stack, with the goal of catching payload drift. Aim to identify that the workload that had a 1KB median payload six months ago is now at 40KB, before it becomes a late-night latency incident.</p>
<p>Valkey 8.0 and 9.0 improved caching architecture meaningfully. I/O threading on the write path and copy avoidance on the read path both reduce event-loop pressure from large payloads. These alone change the performance curve. But knowing where your workload sits on that curve still requires instrumentation, and guardrails that respond to live system state still need to be built on top.</p>
<p>Payload limits are still important, but they’re increasingly becoming a first line of defence rather than the entire strategy. Modern cache systems need observability into traffic shape and guardrails that respond to changing conditions in real time.</p>
<p><em>Based on sessions by <a href="https://www.linkedin.com/in/yiwen-zhang-a62a41158/">Yiwen Zhang</a> (Apple) on large-payload guardrails and observability in Valkey, and <a href="https://www.linkedin.com/in/ovaiskhan/">Ovais Khan</a> (Snap) on migrating 350+ cache clusters from KeyDB to Valkey. Watch the full replays at</em> <a href="https://unlockedconf.io/san-jose-replays"><em>unlockedconf.io/san-jose-replays</em></a><em>.</em></p>]]></content:encoded>
  </item>
  <item>
    <title>Disaggregated LLM Inference, Part 3: Why Your Networking Stack May Not Be Ready</title>
    <link>https://www.gomomento.com/jp/blog/disaggregated-llm-inference-part-3-why-your-networking-stack-may-not-be-ready/</link>
  <dc:creator><![CDATA[Hien Luu]]></dc:creator>
    <pubDate>Wed, 13 May 2026 19:32:15 GMT</pubDate>
  <category><![CDATA[AI/ML]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/disaggregated-llm-inference-part-3-why-your-networking-stack-may-not-be-ready/</guid>
    <description><![CDATA[<p>Disaggregated LLM inference shifts the bottleneck from GPUs to the networking stack, where cache movement and data-plane efficiency now define performance at scale.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/Hien-Luu-Blog-Header-P3-2-1024x512.png" alt=""></p>
<p><a href="https://www.gomomento.com/blog/disaggregated-inference-part-1-when-and-where-to-route/">Parts</a> <a href="https://www.gomomento.com/blog/disaggregated-inference-part-1-when-and-where-to-route/">1</a> and <a href="https://www.gomomento.com/blog/moving-the-kv-cache-without-stalling-the-decode/">2</a> covered when to disaggregate, how requests find the right GPU, and how the KV cache moves between phases. All of that assumes a data plane that can actually carry the bytes.</p>
<p>Even with smart routing above and orchestration policy in the middle, you still have to move multiple gigabytes between GPUs in milliseconds.</p>
<p>The baseline is unambiguously bad: standard PyTorch serialization <a href="https://lmcache.ai/tech_report.pdf">tops out below 1 GB/s</a>, or three-plus seconds of dead air for a 3 GB KV cache. The next instinct is NCCL, but NCCL was built for training: <em>collective</em> patterns (AllReduce, AllGather) across a static topology known at startup. Disaggregated inference wants <em>point-to-point</em> transfers between prefill and decode nodes picked per-request by the router.</p>
<p>Three purpose-built alternatives have emerged, each solving a different pain point. <a href="https://developer.nvidia.com/blog/enhancing-distributed-inference-performance-with-the-nvidia-inference-transfer-library/">NIXL</a> (NVIDIA Inference Xfer Library) is the memory-abstraction layer: it exposes HBM, DRAM, NVMe, and S3 as uniform “memory sections” and negotiates the optimal transport underneath (NVLink, GPUDirect Storage, InfiniBand, RoCE) without the application caring which one is used. <a href="https://uccl-project.github.io/posts/kv-transfer-engine/">UCCL</a> (Unified Collective Communication Library) handles P2P transfers without burning GPU compute on data movement (NCCL uses streaming multiprocessors for the transfer itself; UCCL doesn’t), supports both NVIDIA and AMD GPUs, and exposes both NCCL-style collective and explicit read/write APIs, so you get NCCL’s ergonomics without its SM tax, and skip the out-of-band metadata coordination that NIXL’s read/write path requires. <a href="https://github.com/kvcache-ai/Mooncake">Mooncake’s Transfer Engine</a> is the bandwidth specialist, consulting a hardware topology matrix to pick the most proximate NIC per transfer and route around NUMA bottlenecks. It hits 87 GB/s on 4×200 Gbps and up to 190 GB/s on 8×400 Gbps, roughly 2.4× to 4.6× faster than optimized TCP.</p>
<p>How do they actually stack up? The UCCL team <a href="https://uccl-project.github.io/posts/kv-transfer-engine/">benchmarked all four</a> on a pair of 8-GPU AMD MI300X nodes wired with 400 Gbps NICs (50 GB/s per link). On the 256KB–1MB messages typical of KV transfers, NIXL and UCCL P2P both saturate the link; NCCL/RCCL runs 30–50% slower: the SM tax. The gap closes at 10MB+ messages, where SM overhead amortizes. The surprise: Mooncake TE couldn’t saturate even a single 50 GB/s link at 100MB messages, a tension with its impressive multi-NIC aggregate numbers that the UCCL authors flagged but couldn’t fully explain.</p>
<p>Which one you pick depends on your actual pain: NIXL for heterogeneous memory tiers, UCCL for GPU-efficient P2P transfers across vendors, Mooncake TE for raw aggregate bandwidth when you can parallelize across many NICs.</p>
<h2 id="where-this-is-all-going">Where This Is All Going</h2>
<p>Disaggregation doesn’t eliminate the bottleneck in LLM serving. It moves it. The monolithic era of “buy a bigger GPU” is turning into a distributed-systems era where your scheduling sophistication, the policies governing your cache tiers, and the quality of your data plane matter more than which accelerator you bought. Cache-aware routing, content-hashed tiered storage, RDMA transports: none of these are ML concepts. They’re the same primitives distributed caching platforms have been refining for a decade.</p>
<p>The clearest production signal so far: in March, AWS and Cerebras <a href="https://www.cerebras.ai/press-release/awscollaboration">announced</a> a Bedrock service that runs prefill on AWS Trainium and decode on the Cerebras CS-3 (different vendor, different chip, different phase), stitched together over Elastic Fabric Adapter. The architectural pattern this series has been describing is shipping as a managed product, not a research demo. And when the chips on either side of the wire are built by different companies, the data plane connecting them stops being an implementation detail and becomes the product.</p>
<p>LLM serving infrastructure is becoming a globally-addressable, tiered cache network with a compute layer bolted on top. The teams that ship the best user experiences won’t be the ones with the most H100s. They’ll be the ones who treat their inference pipeline like a cache first and a model-execution engine second, with all the operational discipline that implies. At Momento we’ve watched that discipline emerge in caching systems the hard way, over years of chasing tail latency, congestion, and hit-rate math. It’s surprisingly familiar watching it happen again, one layer up.</p>]]></content:encoded>
  </item>
  <item>
    <title>Disaggregated Inference,Part 2: Moving the KV Cache Without Stalling the Decode</title>
    <link>https://www.gomomento.com/jp/blog/moving-the-kv-cache-without-stalling-the-decode/</link>
  <dc:creator><![CDATA[Hien Luu]]></dc:creator>
    <pubDate>Wed, 06 May 2026 18:09:57 GMT</pubDate>
  <category><![CDATA[AI/ML]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/jp/blog/moving-the-kv-cache-without-stalling-the-decode/</guid>
    <description><![CDATA[<p>The real gains in LLM inference don’t come from faster GPUs, but from orchestrating KV Cache movement so work is reused instead of repeated</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/05/Hien-Luu-Blog-Header-P2-2-1024x512.png" alt=""></p>
<p>In <a href="https://www.gomomento.com/blog/disaggregated-inference-part-1-when-and-where-to-route/">Part 1</a> we covered when disaggregation is worth the trouble and how requests find the right GPU. Once a request is routed, the next problem is moving the KV cache between prefill and decode without stalling the decode stream.</p>
<p>A fast transport moves bytes from prefill to decode. It doesn’t tell you when to start sending, where the cache lives between hops, or how to reuse it across requests. Those are orchestration questions, and they’re where most of the practical performance lives.</p>
<p>The first move is layer-wise streaming. <a href="https://www.usenix.org/system/files/fast25-qin.pdf">Mooncake</a> calls it “Layer-wise Prefill”: as soon as prefill finishes computing the KV cache for layer 0, that layer’s blocks start streaming to the decode node while the prefill GPU is still computing layer N. The transfer hides behind compute that would otherwise stall, dropping visible transfer latency significantly on long prompts.</p>
<p>The second move is treating the KV cache as a tiered store, with hot blocks in HBM, warm blocks in CPU DRAM, and cold blocks on networked SSDs. Mooncake Store keys blocks by content hash, evicts LRU, and replicates hot blocks across nodes so a popular system prompt doesn’t bottleneck on one location. DeepSeek’s <a href="https://github.com/deepseek-ai/open-infra-index">Fire-Flyer File System (3FS)</a> takes it further, using NVMe SSDs over RDMA to break the dependency on node-local DRAM. A 180-node 3FS cluster delivers 6.6 TiB/s aggregate read throughput and 40 GiB/s per client for KV lookups, fast enough that “the cache lives on disk” becomes viable rather than a fallback.</p>
<p>The third move is choosing the handoff itself. DistServe pulls: decode fetches on demand, using prefill memory as a queuing buffer. Mooncake’s Conductor pushes: prefill streams each layer to decode and frees its memory. 3FS-backed designs use shared storage. Push minimizes decode-side latency at the cost of memory pressure on the prefill pool. Pull keeps memory pressure where the work is queued. Shared storage decouples both, at the cost of a network hop.</p>
<p>Perplexity’s <a href="https://www.perplexity.ai/hub/blog/disaggregated-prefill-and-decode">KV Messenger</a> shows what this looks like in production. Built on RDMA via libfabric, it polls a counter that’s incremented after the output projection of each layer. Because that projection reduces across tensor-parallel ranks, the counter implicitly synchronizes them, letting the system track per-layer completion without breaking CUDA graphs. The moment the counter ticks, RDMA writes start flying. The decoder doesn’t even need an explicit completion signal: it counts incoming RDMA operations against the expected total. Serving DeepSeek-R1, mixed prefill-decode struggled to exceed 50 TPS due to prefill interruptions; after disaggregating, a single prefiller kept three decoders saturated at 90+ TPS for a 100ms TTFT cost.</p>
<p>The payoff is reuse. Most tokens in a typical prompt have been processed before, by someone: a system prompt shared across all users of an app, earlier turns of an active conversation, the boilerplate of a few-shot template. When the KV store is content-hashed and tiered across HBM, DRAM, and SSD, those tokens don’t have to be prefilled again. The prefill work that would have been redone on every turn is instead amortized across every request that shares a prefix.</p>
<p>Combine that with the layer-wise streaming above, and the whole shape of the workload changes. This is how <a href="https://www.usenix.org/system/files/fast25-qin.pdf">Moonshot AI runs Kimi at 100+ billion tokens per day, handling 115% more requests</a> than non-disaggregated baselines on the same A800 hardware: orchestration turns what would be a compute problem into a cache problem.</p>
<h2 id="up-next">Up next</h2>
<p>Layer-wise streaming, tiered storage, and clever handoff semantics all assume the data plane underneath can actually deliver multiple gigabytes between GPUs in milliseconds. Picking the right transport for that, and understanding why the obvious choices fall short, is its own problem.</p>
<p><strong>Next in Part 3:</strong> Why Your Networking Stack Might Not Be Ready.</p>]]></content:encoded>
  </item>
</channel>
</rss>
