Two ways to run an 8K request
Long-context generation is dynamic: each token makes the KV cache a little larger. The NPU compiler, however, wants concrete tensor shapes. OpenVINO therefore has to choose what to compile and reserve before inference begins.
| Design | Compiled graphs | KV allocation | Tradeoff |
|---|---|---|---|
| B0 · Direct | one 8K graph | 8K from startup | simple, but small contexts run through a padded 8K shape |
| B1 · Upstream pyramid | 1K / 2K / 4K / 8K | shared 8K maximum | better-sized graphs for short contexts, but every graph and the maximum KV are prepared up front |
B1 solves the graph-shape problem: the runtime can select the smallest compiled graph that fits the current context. It does not solve proportional memory growth. Both baselines reserve an 8K KV allocation for the entire request.
Improvement 1 · Memory grows with contextKeep the pyramid, copy-and-grow the KV
Our first change keeps B1’s 1K/2K/4K/8K graph pyramid but replaces the shared-max KV with capacity-sized allocations. The request starts with 1K. When it crosses a boundary, the runtime allocates the next buffer, copies live KV state forward, binds the next graph, and continues.
Qwen2.5-7B prototype: crossing all three boundaries
The first full prototype successfully grew from 1K to 8K. Copy cost increased with the amount of live state, but remained small when amortized across the 8,064 decoded tokens.
| Boundary | New KV bytes | Migration time | Top-1 | Logit cosine |
|---|---|---|---|---|
| 1K → 2K | +56 MiB | 277.7 ms | 7 / 7 | 0.9999944 |
| 2K → 4K | +112 MiB | 317.0 ms | 7 / 7 | 0.9999960 |
| 4K → 8K | +224 MiB | 449.8 ms | 7 / 7 | 0.9999976 |
Steady-state throughput remained close to the direct 8K baseline rather than collapsing under the extra handoffs:
| Context | Copy-and-grow | Direct 8K | Δ vs direct |
|---|---|---|---|
| 0–1K | 0.32259 tok/s | 0.32066 tok/s | +0.599% |
| 1K–2K | 0.32217 tok/s | 0.32063 tok/s | +0.479% |
| 2K–4K | 0.32203 tok/s | 0.32033 tok/s | +0.529% |
| 4K–8K | 0.32103 tok/s | 0.32041 tok/s | +0.190% |
This was the initial Qwen2.5-7B feasibility run. It establishes mechanism and boundary quality; the replicated N=5 experiment below evaluates the later JIT scheduling change.
What remained after Improvement 1The KV is smaller, but startup is still eager
Three remaining bottlenecks
- Compiled-capacity footprint. The runtime still materializes every 1K/2K/4K/8K artifact before decoding, so unified memory must hold work for capacities the request may never reach.
- Transient peak memory. Copying needs the old and new KV buffers to coexist; the final boundary can temporarily approach two allocations.
- Foreground compilation. Even though only the 1K graph is needed at token one, eager copy-and-grow waits for all four shapes before starting useful NPU work.
A second bottleneck was inside the compiler
Tracing the OpenVINO NPU compiler exposed 33.33 million repeated dependency-vector sorts in the scheduling path; 94.2% became cache hits under memoization. The patch reduced full compile time by 3.34–3.68% in 12/12 production A/B trials.
That optimization makes each specialization cheaper. The runtime change below attacks the orthogonal question: where should the remaining compile time occur?
Compile the next graph while the NPU decodes
One-ahead JIT changes only the schedule. Compile 1K before decode, then launch 2K on the CPU while 1K runs on the NPU. After switching, compile 4K while 2K runs, then repeat for 8K. The tokens remaining before each boundary become a natural latency-hiding window.
T_compile(next) < T_decode(tokens remaining)One-ahead is the balance point. Compiling every future size in parallel recreates the up-front capacity problem. Waiting until the boundary creates a stall. Staying one step ahead bounds speculative work while giving compilation the longest useful runway.
Replicated JIT result on NUC16
We tested Improvement 2 on NUC16 with Intel NPU 5 and OpenVINO 2026.3. The workload used Qwen2.5-0.5B FP16, a 128-token prompt, and 8,064 teacher-forced decoded tokens across the 1K → 2K → 4K → 8K ladder.
I2 treatment = compile 1K first, then compile 2K / 4K / 8K one step ahead
Design = N=5 paired, Williams-order counterbalanced runs
I1 and I2 use the same graphs and the same copy-and-grow KV path. Only compile scheduling changes, isolating how much foreground latency can be hidden behind decoding.
| Endpoint | I1 eager | I2 one-ahead | Paired effect |
|---|---|---|---|
| Foreground compile wall | 84.29 s | 65.33 s | −18.96 s · 22.49% |
| Protocol critical path | 232.43 s | 212.98 s | −19.46 s · 8.37% |
| Decode inference | 143.32 s | 142.94 s | −0.38 s · 0.26% |
| Boundary wait | — | 0.00 s | 15 / 15 ready |
Paired-t 95% CI: compile savings 17.95–19.96 s; critical-path savings 18.71–20.21 s; decode change −1.05–0.29 s. With five pairs, the minimum exact two-sided sign-flip p-value is 0.0625, so we emphasize effect size, interval, replication, and boundary evidence—not a p<.05 claim.
Correctness is a transition property
A model that survives startup can still fail when live state crosses a compiled-shape boundary. Device checks confirmed every successor stayed on the NPU. Boundary logits matched the eager control in all 20 comparisons, with minimum cosine similarity reported as 1.0.
Virtual remapping and broader evaluation
The next systems step is to decouple logical KV addresses from physical storage. A larger specialization could inherit existing pages instead of recopying all prior state, reducing the transient peak at each boundary.
What remains to validate
- Batching: the current end-to-end JIT campaign uses batch size 1.
- Compile decomposition: separately profile and schedule prefill and decode compilation.
- Generality: extend the replicated JIT study across model sizes, context ladders, and precisions.
- Memory lifetime: replace full KV copies with page-aware remapping and measure both average and peak memory.
The idea in one sentence
Start with the smallest useful NPU program, grow its live KV only when needed, and treat each future specialization as a CPU deadline that can be hidden behind the current NPU decode window.