logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

42x faster prompt lookup drafting in llama.cpp

Original article
jadidbourbaki.github.io|14 points|2 comments|by pptadversary|Sep 26, 2026

Optimizing Prompt Lookup Drafting in llama.cpp: A 42x Speedup

By Hayder Tirmazi | Originally published: 2026-09-26 [Homepage] | [GitHub] | [Twitter]


⚡ Executive Summary (TL;DR)

I have implemented a series of performance optimizations—heavily inspired by the research of Martin Ankerl and Daniel Lemire—that accelerate prompt lookup drafting in llama.cpp by up to 42x while simultaneously reducing memory consumption by up to 2.6x.

Update: Following my work, Daniel Lemire submitted a Pull Request that further boosts the speed of prompt lookup drafting by an additional 4.2x on top of my optimizations.


🔍 Understanding Prompt Lookup Decoding

Many high-performance inference engines (such as vllm and llama.cpp) and libraries (like Hugging Face's transformers) utilize prompt lookup decoding. This technique, also known as n-gram speculation, is designed to accelerate the generation of tokens.

Technically, this is a specialized form of speculative decoding. Instead of using a complex secondary model, it uses a sophisticated "really stupid" draft model: the n-gram model.

How it Works

The engine attempts to draft the next kkk tokens. For a 3-gram model, it looks for sequences like:

  • (x1,x2,x3)(x_1, x_2, x_3)(x1​,x2​,x3​)
  • (x2,x3,x4)(x_2, x_3, x_4)(x2​,x3​,x4​)
  • Generally: (xi,xi+1,xi+2)(x_i, x_{i+1}, x_{i+2})(xi​,xi+1​,xi+2​) for any i∈{1,…,t−2}i \in \{1, \ldots, t-2\}i∈{1,…,t−2}.

An n-gram model is probabilistic; it predicts the subsequent token based on the preceding n−1n-1n−1 tokens. By parsing a text corpus into n-grams, the model can simply select the token that most frequently followed that specific sequence in the training data.

The N-Gram Cache

The system relies on an n-gram cache, a data structure storing c(η,y)c(\eta, y)c(η,y), which represents the count of how many times token yyy follows the n-gram η\etaη.

Cache TypeSymbolDescriptionScope
Context Cachecctxc_{\text{ctx}}cctx​Stores n-grams (sizes 1-4) for the current sequence x1,…,xtx_1, \ldots, x_tx1​,…,xt​.Updated in real-time during generation.
Dynamic Cachecdync_{\text{dyn}}cdyn​Stores n-gram counts from previous model executions.Persists across conversations.
Static Cachecstc_{\text{st}}cst​Stores 2-grams from a fixed text corpus.Created via llama-lookup-create.

⚙️ The Drafting Algorithm

To draft a new token, llama.cpp looks at the previous nnn tokens, denoted as Xn=(xt−n+1,…,xt)X_n = (x_{t-n+1}, \ldots, x_t)Xn​=(xt−n+1​,…,xt​).

1. Scoring

For every token yyy in the vocabulary, a score snf(y)s_n^{f}(y)snf​(y) is calculated:

snf(y)=f(Xn,y)⋅w(y)s_n^{f}(y) = f(X_n, y) \cdot w(y)snf​(y)=f(Xn​,y)⋅w(y)

Where the weight w(y)w(y)w(y) is defined as: w(y)={100 cst(X2,y)if cst(X2,y)>01otherwisew(y) = \begin{cases} 100 \, c_{\text{st}}(X_2, y) & \text{if $c_{\text{st}}(X_2, y) > 0$} \\ 1 & \text{otherwise} \end{cases}w(y)={100cst​(X2​,y)1​if cst​(X2​,y)>0otherwise​

Note: fff represents either the context cache (cctxc_{\text{ctx}}cctx​) or the dynamic cache (cdync_{\text{dyn}}cdyn​). The weight w(y)w(y)w(y) ensures that tokens appearing in the static cache are heavily prioritized.

2. Selection and Validation

The token with the highest score is chosen: y∗=arg⁡max⁡ysnf(y)y^* = \arg\max_y s_n^{f}(y)y∗=argmaxy​snf​(y). Let F(Xn)=∑yf(Xn,y)F(X_n) = \sum_y f(X_n, y)F(Xn​)=∑y​f(Xn​,y) be the total occurrences of XnX_nXn​.

The token y∗y^*y∗ is accepted as a draft only if it meets these two conditions:

Condition: F(Xn)≥anandf(Xn,y∗)≥pn F(Xn)F(X_n) \ge a_n \quad \text{and} \quad f(X_n, y^*) \ge p_n \, F(X_n)F(Xn​)≥an​andf(Xn​,y∗)≥pn​F(Xn​)

This means the sequence must appear at least ana_nan​ times, and y∗y^*y∗ must follow it at least pnp_npn​ percent of the time.

3. Hard-coded Thresholds (Release b11182)

The values for ana_nan​ (minimum count) and pnp_npn​ (minimum probability) are as follows:

  • Context Cache:
    • a=(2,2,1,1)a = (2, 2, 1, 1)a=(2,2,1,1)
    • p=(0.66,0.5,0.5,0.5)p = (0.66, 0.5, 0.5, 0.5)p=(0.66,0.5,0.5,0.5)
  • Dynamic Cache:
    • a=(4,3,2,2)a = (4, 3, 2, 2)a=(4,3,2,2)
    • p=(0.75,0.66,0.66,0.66)p = (0.75, 0.66, 0.66, 0.66)p=(0.75,0.66,0.66,0.66)

🛠️ Execution Flow

The drafting process follows a strict priority hierarchy:

Static Cache Fallback Logic: If both context and dynamic caches fail, llama.cpp uses the static cache independently. It selects yyy with the largest cst(X2,y)c_{\text{st}}(X_2, y)cst​(X2​,y) if:

  • Cst(X2)≥2C_{\text{st}}(X_2) \ge 2Cst​(X2​)≥2
  • cst(X2,y)≥0.5 Cst(X2)c_{\text{st}}(X_2, y) \ge 0.5 \, C_{\text{st}}(X_2)cst​(X2​,y)≥0.5Cst​(X2​)

🧪 Experimental Setup

To validate these optimizations, I utilized the example provided in the llama.cpp repository.

Tools Used:

# Create static cache from corpus
llama-lookup-create 

# Benchmark the drafting performance
llama-lookup-stats 

Methodology:

  1. Dataset: I used WikiText-103 to build the static cache.
  2. Simulation: llama-lookup-stats treats the tokens in the test file as if they were generated by a model.
  3. Verification: It iterates through the drafting loop and tracks:
    • Token match rate (Acceptance rate)
    • Drafting latency
    • Static cache load time

Since I did not alter the underlying algorithm, the acceptance rate should remain constant. I verified that my changes maintained nearly identical acceptance rates compared to the original implementation.

Key Metrics for Improvement:

  1. Latencyper_token\text{Latency}_{\text{per\_token}}Latencyper_token​
  2. Timeload_static_cache\text{Time}_{\text{load\_static\_cache}}Timeload_static_cache​

Performance Graph Placeholder Figure 1: Conceptual representation of latency reduction.