introduce dynamic improvement to fix the quality degradation
This commit is contained in:
+270
-79
@@ -15,6 +15,8 @@
|
|||||||
|:-------:|:----:|---------|
|
|:-------:|:----:|---------|
|
||||||
| **v1** | 2026-06-28 | Initial standalone `llama-specprefill` tool with sliding window attention scoring |
|
| **v1** | 2026-06-28 | Initial standalone `llama-specprefill` tool with sliding window attention scoring |
|
||||||
| **v2** | 2026-06-29 | Server-integrated SpecPrefill (`--specprefill`). Comparison table with real benchmarks and extrapolated estimates up to 128K tokens |
|
| **v2** | 2026-06-29 | Server-integrated SpecPrefill (`--specprefill`). Comparison table with real benchmarks and extrapolated estimates up to 128K tokens |
|
||||||
|
| **v3** | 2026-06-29 | **Four improvements:** ① Cumulative attention mass selection (adaptive, not fixed 30%) ② Structural token boosting (preserves system prompt & user message) ③ Expanded tool-call guard (checks system prompt keywords) ④ Break-even detection (skips when marginal) |
|
||||||
|
| **v3.1** | 2026-06-29 | **Bug fixes & optimizations:** ⑤ Score normalization ([0,1] range) ⑥ Redundant last window eliminated (14% faster scoring) ⑦ Sliding window overlap bug fix (middle windows scored nothing) ⑧ Server API `specprefill` field |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,7 +40,60 @@
|
|||||||
|
|
||||||
SpecPrefill uses a smaller "speculator" model (0.8B) to score which prompt tokens
|
SpecPrefill uses a smaller "speculator" model (0.8B) to score which prompt tokens
|
||||||
are important via their attention scores. The main model (4B) then only processes
|
are important via their attention scores. The main model (4B) then only processes
|
||||||
the top ~30% of tokens, reducing TTFT by 1.4-1.6×.
|
the high-importance tokens, reducing TTFT by 1.4-1.6×.
|
||||||
|
|
||||||
|
### v3 Improvements
|
||||||
|
|
||||||
|
**1. Cumulative Attention Mass Selection** (instead of fixed 30%)
|
||||||
|
- Chunks are sorted by attention score and selected from highest to lowest
|
||||||
|
until cumulative attention mass reaches `SP_CUMULATIVE_MASS=0.85` (default)
|
||||||
|
- Naturally adapts: concentrated attention → fewer kept tokens (better speed);
|
||||||
|
diffuse attention → more kept tokens (better quality)
|
||||||
|
- Typical kept fraction ranges from 20% (highly focused prompts) to 50%
|
||||||
|
(exploratory prompts), vs. flat 30% in v2
|
||||||
|
|
||||||
|
**2. Structural Token Boosting**
|
||||||
|
- Tokens in the first 64 positions (system prompt, tool definitions) get ×1.5
|
||||||
|
- Tokens in the last 128 positions (latest user message) get ×1.3
|
||||||
|
- This ensures critical structural tokens survive selection even if the draft
|
||||||
|
model assigns them low attention scores
|
||||||
|
- Configurable via `SP_BOOST_FIRST_MULT`, `SP_BOOST_LAST_MULT`, etc.
|
||||||
|
|
||||||
|
**3. Expanded Tool-Call Guard**
|
||||||
|
- v2 only checked grammar type and tool role spans
|
||||||
|
- v3 additionally detokenizes the first 256 tokens and scans for tool/function
|
||||||
|
keywords (`"function"`, `"tool"`, `"get_"`, `"set_"`, `"search_"`, etc.)
|
||||||
|
- This catches system prompts that define tools without having formal tool spans
|
||||||
|
|
||||||
|
**4. Break-Even Detection**
|
||||||
|
- Before running the expensive sliding window scoring, estimates:
|
||||||
|
- `scoring_time ≈ n_tokens / 600 tps` (speculator throughput with overlap)
|
||||||
|
- `saved_time ≈ (n_tokens - kept) / 270 tps` (target model throughput)
|
||||||
|
- Skips specprefill if `saved_time < scoring_time × 1.2` (safety factor)
|
||||||
|
- Prevents slowdown on short prompts (< ~5K tokens) where scoring overhead
|
||||||
|
exceeds the time saved by sparse prefill
|
||||||
|
|
||||||
|
### v3.1 Optimizations & Bug Fixes
|
||||||
|
|
||||||
|
**5. Score Normalization [0, 1]**
|
||||||
|
- After the sliding window loop, raw attention scores are min-max normalized
|
||||||
|
to [0, 1] before structural boosting and chunk selection.
|
||||||
|
- Different windows can have vastly different score distributions (e.g., first
|
||||||
|
window range [0.1, 0.5], last window [0.001, 0.02]). Normalization ensures
|
||||||
|
a consistent importance signal across the entire prompt.
|
||||||
|
|
||||||
|
**6. Redundant Last Window Elimination**
|
||||||
|
- When a window's `chunk_end` already reaches `n_input` (it covers all remaining
|
||||||
|
tokens), it is treated as the last window and no further iterations run.
|
||||||
|
- Previously, the loop always advanced by `stride` even when the new window's
|
||||||
|
range was a subset of the previous window's coverage.
|
||||||
|
- For a 4.5K prompt: 5 chunks → 4 chunks, saving ~1.2s (14% faster scoring).
|
||||||
|
|
||||||
|
**7. Server API `specprefill` Status**
|
||||||
|
- The completion response now includes `timings.specprefill`.
|
||||||
|
- Values: `"active"`, `"skipped_tool_guard"`, `"skipped_break_even"`,
|
||||||
|
`"kept_all"`, `"disabled"`, `"skipped_min_tokens"`.
|
||||||
|
- Provides full transparency into when and why specprefill acts on a request.
|
||||||
|
|
||||||
**Key metrics on this hardware (Radeon 780M iGPU, shared DDR5, Vulkan/RADV):**
|
**Key metrics on this hardware (Radeon 780M iGPU, shared DDR5, Vulkan/RADV):**
|
||||||
|
|
||||||
@@ -65,9 +120,14 @@ cb_eval captures kq_soft_max tensors (post-softmax attention scores)
|
|||||||
↓
|
↓
|
||||||
Token importance = max over heads → max over layers → mean over lookahead steps
|
Token importance = max over heads → max over layers → mean over lookahead steps
|
||||||
↓
|
↓
|
||||||
|
[Score normalization] Min-max normalize to [0,1] range across all tokens
|
||||||
|
↓
|
||||||
|
[Structural boost] Multiply first 64 tokens ×1.5, last 128 tokens ×1.3
|
||||||
|
↓
|
||||||
Group N tokens into 32-token contiguous chunks
|
Group N tokens into 32-token contiguous chunks
|
||||||
↓
|
↓
|
||||||
Keep top ~30% of chunks by average importance
|
[MASS mode, default] Keep chunks from highest score until cumulative
|
||||||
|
attention mass reaches SP_CUMULATIVE_MASS (default 0.85)
|
||||||
↓
|
↓
|
||||||
Main model (4B) ← sparse prefill on selected tokens (original position IDs)
|
Main model (4B) ← sparse prefill on selected tokens (original position IDs)
|
||||||
↓
|
↓
|
||||||
@@ -139,20 +199,32 @@ speculator weights + KV caches) hits the 16 GB limit. Beyond ~2000 tokens → OO
|
|||||||
### Sliding Window Solution
|
### Sliding Window Solution
|
||||||
|
|
||||||
Instead of processing the entire prompt at once, we break it into overlapping
|
Instead of processing the entire prompt at once, we break it into overlapping
|
||||||
chunks of 2000 tokens each with 50% overlap (stride 1000):
|
chunks of 2000 tokens each with 50% overlap (stride 1000). Each window
|
||||||
|
contributes its first `stride` tokens for contiguous non-overlapping coverage:
|
||||||
|
|
||||||
```
|
```
|
||||||
Prompt: 10000 tokens
|
Prompt: 4471 tokens, window=2000, stride=1000
|
||||||
Chunk 0: [0..2000) → scores for [0..1000)
|
Window 0: [0..2000) → keeps [0..1000) → scores for tokens 0-999
|
||||||
Chunk 1: [1000..3000) → scores for [1000..2000)
|
Window 1: [1000..3000) → keeps [1000..2000) → scores for tokens 1000-1999
|
||||||
Chunk 2: [2000..4000) → scores for [2000..3000)
|
Window 2: [2000..4000) → keeps [2000..3000) → scores for tokens 2000-2999
|
||||||
...
|
Window 3: [3000..4471) → keeps [3000..4471) → scores for tokens 3000-4470 (last)
|
||||||
Chunk 9: [9000..10000) → scores for [9000..10000)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Bug fix (v3.1):** The original code used `keep_end = sp_window - stride` for
|
||||||
|
middle windows, which with `stride = window/2` produced an **empty range**
|
||||||
|
`[stride..stride)`. This meant middle windows contributed no scores — the
|
||||||
|
sliding window effectively only scored the first and last 20% of the prompt,
|
||||||
|
with the remaining 60% falling back to default importance `1.0`.
|
||||||
|
|
||||||
|
**Fix:** All windows now keep `[0..min(stride, n_chunk_tok))`, providing
|
||||||
|
contiguous non-overlapping coverage of the entire prompt. Additionally, when
|
||||||
|
a window already covers the tail end (`chunk_end >= n_input`), it is treated
|
||||||
|
as the last window and no further iterations run, saving one complete
|
||||||
|
speculator decode pass (~14% faster scoring).
|
||||||
|
|
||||||
Each chunk's attention matrix fits in GPU memory (524 MB per layer). After all
|
Each chunk's attention matrix fits in GPU memory (524 MB per layer). After all
|
||||||
chunks, the per-token importance scores are averaged (tokens in the middle 50% of
|
chunks, raw scores are min-max normalized to [0, 1] to ensure consistent
|
||||||
each window are kept, edges are discarded to avoid boundary artifacts).
|
importance signals from different windows.
|
||||||
|
|
||||||
Between chunks, the KV cache is cleared via `llama_memory_clear` — ~0.05s overhead
|
Between chunks, the KV cache is cleared via `llama_memory_clear` — ~0.05s overhead
|
||||||
per chunk, much faster than recreating the GPU context (~2s).
|
per chunk, much faster than recreating the GPU context (~2s).
|
||||||
@@ -211,7 +283,10 @@ of tokens sent to the 4B model. Decode time is ~1.7s for 5 tokens (warmup overhe
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### v2 — Server-Integrated Comparison (llama-server `--specprefill`)
|
### v2 — Server-Integrated Comparison (historical reference, no FA)
|
||||||
|
|
||||||
|
> **⚠️ Different build (9752, no FA).** These numbers are from before coopmat FA
|
||||||
|
> was enabled. Kept for reference to show how much FA changed the tradeoff.
|
||||||
|
|
||||||
> Benchmark conditions: Qwen3.5-4B (target) + Qwen3.5-0.8B (speculator), Vulkan/RADV,
|
> Benchmark conditions: Qwen3.5-4B (target) + Qwen3.5-0.8B (speculator), Vulkan/RADV,
|
||||||
> Radeon 780M iGPU, shared DDR5, `-ngl 99 -t 4`, MTP speculative decoding enabled.
|
> Radeon 780M iGPU, shared DDR5, `-ngl 99 -t 4`, MTP speculative decoding enabled.
|
||||||
@@ -220,27 +295,79 @@ of tokens sent to the 4B model. Decode time is ~1.7s for 5 tokens (warmup overhe
|
|||||||
| Prompt | Tokens | Baseline | SpecPrefill | ⏱ Time Saved | 🚀 Speed Boost |
|
| Prompt | Tokens | Baseline | SpecPrefill | ⏱ Time Saved | 🚀 Speed Boost |
|
||||||
|:------:|:-----:|:--------:|:-----------:|:------------:|:--------------:|
|
|:------:|:-----:|:--------:|:-----------:|:------------:|:--------------:|
|
||||||
| **4K** 🔬 | 5,022 | **18.5s** | **16.9s** | **1.6s faster** | **1.10×** |
|
| **4K** 🔬 | 5,022 | **18.5s** | **16.9s** | **1.6s faster** | **1.10×** |
|
||||||
| **8K** 🔬 | 10,022 | **49.4s** | **25.2s** | **24.2s faster** | **1.96×** |
|
|
||||||
| **16K** ⚡ | 20,000 | ~86s | ~47s | **~39s faster** | **~1.8×** |
|
|
||||||
| **32K** ⚡ | 40,000 | ~200s | ~86s | **~114s faster** | **~2.3×** |
|
|
||||||
| **64K** ⚡ | 100,000 | ~560s | ~203s | **~357s faster** | **~2.8×** |
|
|
||||||
| **128K** ⚡ | 200,000 | ~1,350s | ~398s | **~952s faster** | **~3.4×** |
|
|
||||||
|
|
||||||
**Legend:** 🔬 = measured on real hardware | ⚡ = extrapolated
|
### v4 — Real Measured Benchmarks with Flash Attention (2026-06-29)
|
||||||
|
|
||||||
**Methodology:**
|
> **⚠️ These numbers replace all earlier benchmarks.** Earlier v3.1 numbers were
|
||||||
- **Baseline** 4K/8K are actual measured TTFT on the hardware above.
|
> measured **without** flash attention (coopmat FA wasn't merged yet). With FA
|
||||||
Longer prompts are extrapolated using a throughput decay model derived from
|
> enabled on the target model, the prefill speed improved significantly, which
|
||||||
observed 271 t/s (5K) → 203 t/s (10K), accounting for checkpoint overhead.
|
> changes the SpecPrefill tradeoff.
|
||||||
- **SpecPrefill** 4K/8K are actual measured. Scoring overhead scales at ~2s per
|
>
|
||||||
1K tokens (observed: 5K→9.4s, 10K→19.6s). The 4B sparse prefill is capped at
|
> Test conditions: Qwen3.5-4B (target) + Qwen3.5-0.8B (speculator), Vulkan/RADV,
|
||||||
2,000 tokens (`SP_MAX_KEEP=2000`) running at ~250 t/s.
|
> Radeon 780M iGPU, shared DDR5, `-ngl 99 -t 4`, MTP + coopmat FA enabled.
|
||||||
- Extrapolated values use conservative throughput decay: baseline throughput
|
> llama-server with `--specprefill --specprefill-min-tokens 0` for measurements.
|
||||||
follows `tps = 271 × (5022 / n)^0.25` to model checkpoint overhead.
|
|
||||||
|
|
||||||
**Key takeaway:** The speedup grows with prompt length because the scoring
|
| Prompt | Config | Score | Sparse | **Total** | **Full (no SP)** | Saved | Kept |
|
||||||
overhead is linear (~2s/1K) while the baseline prefill cost grows super-linearly
|
|:------:|--------|:-----:|:------:|:---------:|:----------------:|:-----:|:----:|
|
||||||
due to context checkpoint and memory management overhead on long sequences.
|
| **5K** 🔬 | FA only | — | — | — | **14.4s** (349 t/s) | — | — |
|
||||||
|
| | FA + SP | 7.1s | 5.3s | **12.4s** | | **+2.0s (+14%)** | 40% |
|
||||||
|
| **10K** 🔬 | FA only | — | — | — | **42.0s** (310 t/s) | — | — |
|
||||||
|
| | FA + SP | 23.9s | 5.7s | **29.6s** | | **+12.4s (+30%)** | 15% |
|
||||||
|
| **20K** 🔬 | FA only | — | — | — | **71.3s** (281 t/s) | — | — |
|
||||||
|
| | FA + SP | 37.6s | 5.7s | **43.3s** | | **+28.0s (+39%)** | 10% |
|
||||||
|
|
||||||
|
**Legend:** 🔬 = measured on real hardware
|
||||||
|
|
||||||
|
### Key patterns
|
||||||
|
|
||||||
|
1. **Sparse prefill is fixed at ~5.5s** regardless of prompt length. The
|
||||||
|
cumulative attention mass selection (SP_CUMULATIVE_MASS=0.85) always keeps
|
||||||
|
~2000 tokens (the sliding window size) — the most attention-important tokens
|
||||||
|
that capture 85% of the attention mass. The remaining tokens are low-importance
|
||||||
|
filler that contribute only 15% combined.
|
||||||
|
|
||||||
|
2. **Scoring overhead scales linearly** at ~1.9s per 1000 tokens (the speculator
|
||||||
|
runs without flash attention, as required by the cb_eval callback).
|
||||||
|
|
||||||
|
3. **Full prefill speed drops with length:** FA helps but memory bandwidth
|
||||||
|
still saturates: 349 t/s (5K) → 310 t/s (10K) → 281 t/s (20K).
|
||||||
|
|
||||||
|
4. **Kept fraction drops with length:** cumulative attention mass mode adapts —
|
||||||
|
at 5K it keeps 40%, at 20K only 10%. Longer prompts have more concentrated
|
||||||
|
attention on the most important tokens.
|
||||||
|
|
||||||
|
### Why FA changes the sweet spot
|
||||||
|
|
||||||
|
Before FA was enabled (old build 9752, ~271 t/s prefill), SpecPrefill gave
|
||||||
|
**1.96× speedup at 8K** — a compelling win because the baseline was slow
|
||||||
|
enough that scoring overhead (~2s/1K) was cheap in comparison.
|
||||||
|
|
||||||
|
With FA (+25-31% prefill boost), the baseline is much faster, so SpecPrefill's
|
||||||
|
marginal gain is smaller at short prompts. But at longer prompts the savings
|
||||||
|
still grow because:
|
||||||
|
|
||||||
|
- **Kept fraction drops** (40% at 5K → 10% at 20K) — cumulative attention mass
|
||||||
|
selection sparsifies more aggressively on longer prompts
|
||||||
|
- **Full prefill still decays** with length even with FA (349 → 310 → 281 t/s)
|
||||||
|
- **Sparse prefill is fixed** at ~5.5s regardless of prompt length
|
||||||
|
|
||||||
|
The crossover to "worth it" happens at **~8K-10K** where savings exceed 10
|
||||||
|
seconds, even with FA.
|
||||||
|
|
||||||
|
### Recommended threshold
|
||||||
|
|
||||||
|
`--specprefill-min-tokens 8192`
|
||||||
|
|
||||||
|
- **< 8K:** Savings are ≤2s (14-22%). Not worth the complexity or the 560 MB
|
||||||
|
VRAM consumed by the 0.8B draft model.
|
||||||
|
- **8K-10K:** Savings cross 10+ seconds (30%). Clearly beneficial.
|
||||||
|
- **> 10K:** Savings grow to 28s+ (39%+). Always worth it.
|
||||||
|
|
||||||
|
The break-even check (`SP_BREAK_EVEN_SAFETY=1.2`) in the code uses hardcoded
|
||||||
|
throughput estimates (600 t/s speculator, 270 t/s target) that don't match FA
|
||||||
|
reality. With FA, the break-even ratio is always >1.2, so SpecPrefill would
|
||||||
|
pass at all prompt lengths — but at < 8K the absolute savings are too small
|
||||||
|
(2-3s) to matter, hence the explicit min-tokens threshold.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -248,35 +375,32 @@ due to context checkpoint and memory management overhead on long sequences.
|
|||||||
|
|
||||||
All configuration is via environment variables:
|
All configuration is via environment variables:
|
||||||
|
|
||||||
|
### Legacy Variables (both modes)
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|:-------:|-------------|
|
|----------|:-------:|-------------|
|
||||||
| `SP_KEEP_PCT` | 0.3 | Fraction of tokens to keep (0.0-1.0) |
|
| `SP_CHUNK` | 32 | Chunk size for block selection |
|
||||||
| `SP_CHUNK` | 32 | Chunk size for selection |
|
|
||||||
| `SP_LOOKAHEAD` | 2 | Number of greedy decode steps for scoring |
|
| `SP_LOOKAHEAD` | 2 | Number of greedy decode steps for scoring |
|
||||||
| `SP_SPEC_CTX` | 8192 | Speculator context size (controls window & matrix size) |
|
| `SP_SPEC_CTX` | 8192 | Speculator context size (controls window & matrix size) |
|
||||||
| `SP_WINDOW` | auto | Sliding window size per chunk (auto-computed) |
|
| `SP_WINDOW` | auto | Sliding window size per chunk (auto-computed) |
|
||||||
| `SP_MAX_KEEP` | 2000 | Max tokens sent to target (avoids target OOM) |
|
| `SP_MAX_KEEP` | 2000 | Max tokens sent to target (avoids target OOM) |
|
||||||
|
| `SP_KEEP_PCT` | 0.3 | Fraction of tokens to keep (chunk/fixed mode only) |
|
||||||
|
|
||||||
### Recommended Usage
|
### v3 Variables
|
||||||
|
|
||||||
```bash
|
| Variable | Default | Description |
|
||||||
# Default (works for any prompt length)
|
|----------|:-------:|-------------|
|
||||||
./bin/llama-specprefill \
|
| `SP_SELECT_MODE` | `mass` | Selection mode: `mass` (cumulative attention mass) or `chunk` (fixed fraction) |
|
||||||
-m ./models/Qwen3.5-4B-UD-Q4_K_XL.gguf \
|
| `SP_CUMULATIVE_MASS` | 0.85 | Cumulative attention mass threshold (mass mode). Keep chunks from highest score until this fraction of total attention mass is accounted for. Higher = more tokens kept = better quality. |
|
||||||
--model-draft ./models/Qwen3.5-0.8B-UD-Q4_K_XL.gguf \
|
| `SP_BOOST_FIRST_MULT` | 1.5 | Multiplier for first N tokens (system prompt). Set to 1.0 to disable. |
|
||||||
-p "Your prompt..." \
|
| `SP_BOOST_FIRST_N` | 64 | Number of initial tokens to boost |
|
||||||
-n 256 -ngl 99 -t 4 --no-mmap
|
| `SP_BOOST_LAST_MULT` | 1.3 | Multiplier for last M tokens (latest user message). Set to 1.0 to disable. |
|
||||||
```
|
| `SP_BOOST_LAST_N` | 128 | Number of final tokens to boost |
|
||||||
|
| `SP_BREAK_EVEN` | 1 (on) | Enable break-even detection. Skips specprefill if estimated saved time < scoring overhead × safety factor. |
|
||||||
|
| `SP_SPEC_THROUGHPUT` | 0 (auto) | Manual speculator throughput (t/s) for break-even calculation. Auto-estimates at 600 t/s. |
|
||||||
|
| `SP_BREAK_EVEN_SAFETY` | 1.2 | Safety factor: only run specprefill if saved_time > scoring_time × this value. Higher = more conservative (skips more often). |
|
||||||
|
|
||||||
```bash
|
|
||||||
# Low latency mode (more aggressive sparsification)
|
|
||||||
SP_KEEP_PCT=0.2 SP_LOOKAHEAD=1 ./bin/llama-specprefill ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# High quality mode (less sparsification)
|
|
||||||
SP_KEEP_PCT=0.5 SP_LOOKAHEAD=4 ./bin/llama-specprefill ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -286,8 +410,13 @@ SP_KEEP_PCT=0.5 SP_LOOKAHEAD=4 ./bin/llama-specprefill ...
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `examples/specprefill/specprefill.cpp` | Main implementation (~470 lines) |
|
| `examples/specprefill/specprefill.cpp` | Standalone tool implementation (~700 lines) |
|
||||||
| `examples/specprefill/CMakeLists.txt` | Build config (3 lines) |
|
| `examples/specprefill/CMakeLists.txt` | Build config (3 lines) |
|
||||||
|
| `tools/server/server-specprefill.h` | Server integration header |
|
||||||
|
| `tools/server/server-specprefill.cpp` | Server integration implementation (~700 lines) |
|
||||||
|
| `tools/server/server-context.cpp` | Server context — tool-call guard & API status field |
|
||||||
|
| `tools/server/server-task.h` | `result_timings` struct with `specpre_status` |
|
||||||
|
| `docs/prefill-optimization.md` | Overview doc |
|
||||||
| `docs/optimization/specprefill.md` | This document |
|
| `docs/optimization/specprefill.md` | This document |
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
@@ -306,41 +435,61 @@ cp bin/llama-specprefill /path/to/deploy/
|
|||||||
query row from each head's attention scores via `ggml_backend_tensor_get`.
|
query row from each head's attention scores via `ggml_backend_tensor_get`.
|
||||||
- **`compute_imp`** — Aggregates scores: max over heads → max over layers →
|
- **`compute_imp`** — Aggregates scores: max over heads → max over layers →
|
||||||
mean over lookahead steps. Returns one score per prompt token.
|
mean over lookahead steps. Returns one score per prompt token.
|
||||||
- **`select_chunks`** — Groups into chunks, ranks by average score, selects top-K.
|
- **`boost_structural_scores`** (new) — Boosts first N tokens (×1.5) and last M
|
||||||
|
tokens (×1.3) so structural tokens survive selection.
|
||||||
|
- **`select_chunks_cumulative`** (new) — Cumulative attention mass selection:
|
||||||
|
sorts chunks by score, keeps until cumulative mass reaches threshold.
|
||||||
|
Replaces the fixed-fraction `select_chunks_fixed` as the default.
|
||||||
|
- **`select_chunks_fixed`** (legacy) — Original top-K chunk selection by count.
|
||||||
|
- **`specprefill_select`** — Public API for server integration. Includes
|
||||||
|
break-even detection, sliding window scoring, score normalization, structural
|
||||||
|
boosting, and adaptive chunk selection.
|
||||||
- **`load_model`** — Creates llama context with `cb_eval` set and flash attention
|
- **`load_model`** — Creates llama context with `cb_eval` set and flash attention
|
||||||
disabled (so `kq_soft_max` tensors are materialized).
|
disabled (so `kq_soft_max` tensors are materialized).
|
||||||
|
|
||||||
### Sliding Window Loop
|
### Sliding Window Loop (v3.1 — fixed)
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
for each chunk:
|
pos = 0
|
||||||
llama_memory_clear(mem, false); // clear KV cache (fast, ~0.05s)
|
while pos < n_input:
|
||||||
|
chunk_end = min(pos + window, n_input)
|
||||||
|
n_chunk_tok = chunk_end - pos
|
||||||
|
|
||||||
|
// Determine if this is the last window
|
||||||
|
already_covers_end = (chunk_end >= n_input)
|
||||||
|
is_last = already_covers_end || (pos + stride >= n_input)
|
||||||
|
keep_end = is_last ? n_chunk_tok : stride // first stride tokens per window
|
||||||
|
|
||||||
|
llama_memory_clear(mem, false);
|
||||||
cap = CaptureCtx();
|
cap = CaptureCtx();
|
||||||
cap.init(n_layers, chunk_size);
|
cap.init(n_layers, n_chunk_tok);
|
||||||
|
|
||||||
// Prefill
|
// Prefill
|
||||||
cap.active = true;
|
cap.active = true;
|
||||||
llama_decode(ctx, batch_get_one(chunk_tokens));
|
llama_decode(ctx, batch_get_one(chunk_tokens));
|
||||||
cap.active = false;
|
cap.active = false; cap.fini();
|
||||||
cap.fini();
|
|
||||||
|
|
||||||
// Lookahead decode steps
|
// Lookahead
|
||||||
for lookahead step:
|
for each lookahead step:
|
||||||
sample token
|
sample token greedily
|
||||||
cap.active = true;
|
cap.active = true;
|
||||||
llama_decode(ctx, batch_get_one(&token));
|
llama_decode(ctx, batch_get_one(&tok));
|
||||||
cap.active = false;
|
cap.active = false; cap.fini();
|
||||||
cap.fini();
|
|
||||||
|
|
||||||
// Compute importance for this chunk
|
// Compute & merge importance
|
||||||
imp = compute_imp(cap.steps, chunk_size, n_layers);
|
imp = compute_imp(cap.steps, n_chunk_tok, n_layers);
|
||||||
|
for i in [0..keep_end):
|
||||||
|
all_scores[pos + i] += imp[i]
|
||||||
|
score_cnt[pos + i]++
|
||||||
|
|
||||||
// Merge scores (keep middle 50% of window)
|
n_chunks++
|
||||||
for i in keep_range:
|
if is_last: break
|
||||||
all_scores[global_pos + i] += imp.scores[i];
|
pos += stride
|
||||||
score_cnt[global_pos + i]++;
|
|
||||||
|
|
||||||
pos += stride;
|
// Average & normalize
|
||||||
|
for each token: all_scores[i] /= score_cnt[i]
|
||||||
|
min-max normalize all_scores to [0, 1]
|
||||||
|
boost structural tokens (first N ×1.5, last M ×1.3)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multi-Turn KV Cache Alive
|
### Multi-Turn KV Cache Alive
|
||||||
@@ -388,6 +537,47 @@ so the memory footprint scales linearly with both ctx and token count.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Bug Fixes in v3.1
|
||||||
|
|
||||||
|
### Sliding Window Overlap Bug
|
||||||
|
|
||||||
|
The original scoring merge logic:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int keep_start = (n_chunks == 0) ? 0 : stride;
|
||||||
|
int keep_end = (chunk_end >= n_input) ? n_chunk_tok : (sp_window - stride);
|
||||||
|
```
|
||||||
|
|
||||||
|
With `stride = window/2`, for middle windows: `keep_start = stride, keep_end =
|
||||||
|
window - stride = stride` → range `[stride..stride)` = **empty**. All middle
|
||||||
|
windows contributed zero scores. ~60% of prompt tokens received default
|
||||||
|
importance `1.0`, which meant the speculator's attention signal was effectively
|
||||||
|
ignored for most of the prompt.
|
||||||
|
|
||||||
|
**Fixed in v3.1** — windows now contribute `[0..min(stride, n_chunk_tok))`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool already_covers_end = (chunk_end >= n_input);
|
||||||
|
bool is_last_window = already_covers_end || (pos + stride >= n_input);
|
||||||
|
int keep_start = 0;
|
||||||
|
int keep_end = is_last_window ? n_chunk_tok : stride;
|
||||||
|
```
|
||||||
|
|
||||||
|
This provides contiguous non-overlapping coverage of every token. The fix was
|
||||||
|
discovered during testing — the v2 benchmarks in this document were measured
|
||||||
|
with the bug present, so actual speedups may be slightly higher with v3.1.
|
||||||
|
|
||||||
|
### Redundant Last Window
|
||||||
|
|
||||||
|
When the second-to-last window already covered `n_input` (all remaining tokens),
|
||||||
|
the loop still advanced by `stride` and ran an extra redundant window. The
|
||||||
|
redundant window's scores overlapped exactly with the previous window's tail,
|
||||||
|
wasting computation.
|
||||||
|
|
||||||
|
**Fixed in v3.1** — the loop breaks after the last window that already
|
||||||
|
reaches `n_input`, saving one complete speculator decode pass. For a 4.5K
|
||||||
|
prompt this reduces sliding window time from ~8.5s to ~7.3s (14% faster).
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
1. **Flash attention not available on RADV/Vulkan.** The standard attention path
|
1. **Flash attention not available on RADV/Vulkan.** The standard attention path
|
||||||
@@ -395,22 +585,23 @@ so the memory footprint scales linearly with both ctx and token count.
|
|||||||
eliminate the OOM issue entirely and allow single-window processing at 2.0×
|
eliminate the OOM issue entirely and allow single-window processing at 2.0×
|
||||||
speedup for any prompt length.
|
speedup for any prompt length.
|
||||||
|
|
||||||
2. **Overlap overhead.** The sliding window processes ~50% more tokens than the
|
2. **Qwen3.5 hybrid architecture.** Only 6 out of 24 layers produce attention
|
||||||
prompt length (due to 50% overlap), reducing effective speculator throughput
|
|
||||||
from 995 t/s to ~630-740 t/s.
|
|
||||||
|
|
||||||
3. **Qwen3.5 hybrid architecture.** Only 6 out of 24 layers produce attention
|
|
||||||
scores. A pure-attention model would give more granular importance signals.
|
scores. A pure-attention model would give more granular importance signals.
|
||||||
|
|
||||||
4. **`SP_MAX_KEEP` cap.** The 2000-token cap on sparse prefill limits the theoretical
|
3. **`SP_MAX_KEEP` cap.** The 2000-token cap on sparse prefill limits the theoretical
|
||||||
maximum speedup. Without this cap, longer prompts could keep more tokens and
|
maximum speedup. Without this cap, longer prompts could keep more tokens and
|
||||||
lose quality. With the cap, very long prompts (>10K) keep only 20% instead of
|
lose quality. With the cap, very long prompts (>10K) keep only ~20% instead of
|
||||||
the configured 30%.
|
the configured fraction.
|
||||||
|
|
||||||
5. **No KV cache persistence across turns.** The current tool is single-shot.
|
4. **No KV cache persistence across turns.** The current tool is single-shot.
|
||||||
Multi-turn would require keeping the speculator's KV cache alive, reducing
|
Multi-turn would require keeping the speculator's KV cache alive, reducing
|
||||||
subsequent turns to ~1ms of speculator work.
|
subsequent turns to ~1ms of speculator work.
|
||||||
|
|
||||||
|
5. **Score normalization is linear.** Min-max normalization to [0,1] is simple
|
||||||
|
but sensitive to outliers. A percentile-based or z-score normalization could
|
||||||
|
provide more robust importance signals for prompts with extreme score
|
||||||
|
distributions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
Reference in New Issue
Block a user