Skip to content

[Huginn] Opt-in prefix caching with position-keyed recurrent-state noise (stacked on #147) - #152

Closed
0z5a wants to merge 2 commits into
InferMatrixLab:mainfrom
0z5a:huginn/prefix-cache-position-noise
Closed

0z5a wants to merge 2 commits into
InferMatrixLab:mainfrom
0z5a:huginn/prefix-cache-position-noise

Conversation

@0z5a

@0z5a 0z5a commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #147 (it uses the per-plane layer map); review only the top commit.

Huginn could never reuse a prefix. publish_prefix required a contiguous written prefix in every layer of every plane, and Huginn's prelude/coda layers write plane 0 only, so no Huginn page was ever published. Coverage is now checked per plane over the layers it holds (all layers at depth 0, the recurrent layers deeper); fully recurrent models are unchanged.

A cache hit still changed outputs. Huginn draws each token's initial latent state from a per-request RNG stream in processing order, so a request whose first k prompt tokens come from the cache draws k fewer values, and every later token gets different noise than in a cold run (the prefill chunk size changes the draws the same way). With VLLM_RLT_HUGINN_POSITION_NOISE=1 the noise is a function of (request seed, position, channel): a stage-wise SplitMix64-style hash gives a 24-bit uniform, and an inverse normal CDF maps it to the same truncated normal (std = initializer_range, ±3σ). 64-bit wrap-around is identical on CPU and CUDA. It keeps the distribution but changes which samples a seeded run sees, so it is opt-in, together with VLLM_RLT_HUGINN_PREFIX_CACHE=1 for the engine guard.

Measurements

GH200, Huginn-0125 (32 loops, graphs), 16 agent sessions, at most 4 turns, prompts up to 2000 tokens, 59 requests, one run; measured with plane packing, packed prefill and fused decode loops, position noise on in both arms:

tok/s TTFT p50 TTFT p90 hit rate
prefix cache off 13.8 1.93 s 6.00 s 0%
prefix cache on 26.3 (1.91×) 0.44 s 1.90 s 78.1%
main (no packing, per-row prefill, no cache) 12.4 5.83 s 11.76 s —

Cached vs cold on the GPU (GH200, Huginn-0125, 32 loops, CUDA graphs, packed prefill, eight 512-token prompts run one at a time, 48 greedy tokens; the second run reuses 496 prompt tokens from the cache):

full prefix hit = cold longer prompt, partial hit = cold
position-keyed noise 6/8 8/8
sequential noise (upstream) 6/8 8/8

Position keys removed the one divergence caused by the noise order (a prompt that changed at its first token with the sequential generator matched with position keys). Two divergences remain and come from prefill arithmetic: a cached run prefills only the suffix, so its GEMMs see fewer rows, and one of the two prompts also changed when only the prefill chunk size changed (15/16 cold runs matched between chunks of 2048 and 128 tokens). On the GPU, reuse therefore behaves like a change in batch composition on top of the change of samples. The noise itself matters at 32 loops: the two generators gave the same tokens for 11/16 cold prompts.

Platforms. GH200: torch 2.14.0+cu130, Triton 3.8.0, NIXL 1.4.1. RTX 5090: torch 2.13.0+cu130, Triton 3.7.1. Models: Ouro-1.4B (24 layers, 4 loops, fixed depth) and Huginn-0125 (2 prelude + 4 recurrent + 2 coda layers, 32 loops). Burst workloads issue C requests at once with random-token prompts; session workloads replay multi-turn agent traces with the prefix cache on.

Tests

  • CPU suite: 839 passed; ruff passes. New tests: cached and cold runs, and prefill chunks of 2 and 8 tokens, give identical tokens with position noise; the noise is batch-independent, seed- and position-keyed, free of seed/channel aliasing, and has the truncated-normal moments and tail mass; publication covers only the layers each plane holds.

Notes

The GPU numbers come from a development branch that contains all changes of this series; most cells are single runs, and on the shared GH200 host two runs of main at 32 requests differed by up to 16%, so smaller differences are ties. This branch alone was checked with the CPU suite and ruff.

Series. Draft PRs from one study of serving looped models. They are independent except #152, which is stacked on #147; several touch the same files, so later ones may need a rebase after earlier ones merge.

0z5a and others added 2 commits October 25, 2026 17:20
LAST_EXITED keeps one KV plane per recurrence depth and reserves every layer's
slot in every plane. For a model whose prelude and coda layers run once per
token (Huginn), only plane 0 ever holds those layers: of Huginn-0125's
8 layers x 32 planes = 256 layer slots per token, 124 are never written.
The pool fills with dead slots, so few requests fit and throughput stops
growing with concurrency.

With CacheConfig.pack_recurrent_planes (default on) each plane maps only the
layer slots it can hold: plane 0 all layers, deeper planes the recurrent ones.
A page is a run of blocks of num_layers slots, entry = block * num_layers +
slot in a flattened view of the pool, so a Huginn page needs 17 blocks
instead of 32. Writes, attention, early-exit plane copies and the async
finalize kernel take an entry stride and first-layer offset; PD sends each
plane's live slot range (plane_slots, checked on connect) and the
coordinator prices requests in blocks per page. Values never change, only
their addresses. Fully recurrent models (Ouro) keep the old layout.
--no-pack-recurrent-planes restores the unpacked layout.

GH200, Huginn-0125 (32 loops, CUDA graphs, sync), same KV pool of 24,165
blocks, 512-token prompts, 64 output tokens, one run per level:
  tokens in the pool: 12,080 unpacked -> 22,736 packed (1.88x)
  throughput: 0.99x / 0.99x / 1.14x / 1.16x / 1.06x at 1 / 8 / 32 / 64 / 128
  median TTFT at 64 requests: 15.7 -> 10.9 s
  2048-token prompts: 1.13x / 1.11x at 8 / 32
  token identity: 233/233 requests
PD: 2.79 MB per prompt token sent instead of 5.41 MB (only live slots).
Tests: CPU unit tests for the packed layout, routing, PD segments and slot
checks; GPU test comparing packed and unpacked Huginn (tiny model, CUDA
graphs, early-exit copies) token for token.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacked on the plane-packing change (it uses the per-plane layer map).

Huginn could never reuse a prefix. publish_prefix required a contiguous
written prefix in every layer of every plane, and Huginn's prelude/coda layers
write plane 0 only, so no Huginn page was ever published. Coverage is now
checked per plane over the layers it holds (all layers at depth 0, the
recurrent layers deeper); fully recurrent models are unchanged.

A cache hit still changed outputs: Huginn draws each token's initial latent
state from a per-request RNG stream in processing order, so a request whose
first k prompt tokens come from the cache draws k fewer values and every later
token gets different noise than in a cold run (the prefill chunk size changes
the draws the same way). With VLLM_RLT_HUGINN_POSITION_NOISE=1 the noise is a
function of (request seed, position, channel): a stage-wise SplitMix64-style
hash gives a 24-bit uniform, and an inverse normal CDF maps it to the same
truncated normal (std = initializer_range, +-3 sigma). 64-bit wrap-around is
identical on CPU and CUDA. It keeps the distribution but changes which samples
a seeded run sees, so it is opt-in, together with
VLLM_RLT_HUGINN_PREFIX_CACHE=1 for the engine guard.

CPU tests: cached and cold runs, and prefill chunks of 2 and 8 tokens, give
identical tokens; noise is batch-independent, seed- and position-keyed, and
has the truncated-normal moments and tail mass.

GH200, Huginn-0125 (32 loops, graphs), 16 agent sessions, at most 4 turns,
prompts up to 2000 tokens, 59 requests, one run; measured together with plane
packing, packed prefill and fused decode loops, with position noise on in
both arms:
  prefix cache off: 13.8 tok/s, TTFT p50 1.93 s, p90 6.00 s
  prefix cache on:  26.3 tok/s (1.91x), TTFT p50 0.44 s, p90 1.90 s,
                    78.1% of prompt tokens served from the cache
Cached-vs-cold token identity has been checked on the CPU only so far.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant