Released snapshot aliases hand KV pages back with stale row bytes; masked attention turns them into NaN (device error 6 under KV pressure)
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
Hướng nghiên cứu
Start with runtime_package/cache_memory.rs and scheduler/cache_memory.rs to trace how release_snapshot returns aliased pages to the KV free list, then follow the executor path that launches model work. The report's tested fix queues released pages and zeroes them on the model stream before reuse; compare that lifecycle with the single-GPU reference commit. Done means the cached-prompt reproduction no longer fails under KV pressure, including the multi-process tensor-parallel executor.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
When the KV pool fills, requests that resume from the prefix cache start failing with prepared model reported device error 6. The cause is stale snapshot bytes in reused KV pages:
- Snapshot rows borrow KV page bytes (
runtime_package/cache_memory.rs). - When the last owner releases a snapshot,
release_snapshotreturns its pages to the free list. Nothing clears the row bytes still in them. - A request then receives one of those pages as its partly filled tail page. Positions past the request's last token keep the old row bytes, and in the KV format some of them decode as NaN.
- Attention masks those positions, but a masked position still contributes
0 × NaN = NaNto P·V. The target logits come out non-finite. PreparedStochasticVerificationmarks the row invalid, and the commit reportsINVALID_SPECULATION(6).
It only shows up once pages cycle between snapshot rows and KV use, which is why it needs a full pool. That makes it hit long multi-turn and agent workloads, the ones the prefix cache is for.
This may also explain #61. That issue fails the same way (verification rejects the row, then the commit reports INVALID_SPECULATION), and its failing target column sits in a newly granted page. We haven't tested Gemma 4.
Environment
- DGX Spark (GB10, SM121), single GPU, CUDA 13.0
nvidia/Qwen3.8-27B-NVFP4, exported with theqwen38_denserecipe defaults: 32 requests, MTP up to 5 drafts, FP8 KV- Flare
ca1f960eplus the scratch-copy fix from 317c1ec. The code involved is unchanged onmain(6256cf08). flare run PACKAGE --gpus 0with default flags (adaptive MTP verification on)
Reproduction
The repro runs on a fresh server. It sends alternating distinct ~16k- and ~32k-token prompts. Each prompt is sent once with max_tokens=1 to cache it, then again with max_tokens=64. Requests run one at a time. We used the low-entropy prompts from SPEED-Bench's throughput split.
| Server | First failure |
|---|---|
--memory-limit 50G (3,760 KV pages, 101 snapshot slots) |
prompt 15–17, every run, about 5 minutes in |
| Default memory | prompt 67, nine prompts after the pool first filled |
The failing request is always the second (cached) send. Under --memory-limit 50G, the 101 snapshot slots' alias ranges overlap and together cover every KV page.
Repro script
"""Feed a fresh Flare server distinct long prompts (prime with 1 token, then resend with 64) until the first failure.
Usage: repro.py OUT.csv DATA_DIR (DATA_DIR holds SPEED-Bench throughput_16k.jsonl / throughput_32k.jsonl)"""
import csv, json, re, sys, time, urllib.error, urllib.request
out, data = sys.argv[1], sys.argv[2]
prompts = [[(ctx, r["messages"]) for r in map(json.loads, open(f"{data}/throughput_{ctx}.jsonl"))
if r["category"] == "low_entropy"] for ctx in ("16k", "32k")]
order = [p for pair in zip(*prompts) for p in pair]
KEYS = ("flare_free_kv_tokens", "flare_cached_kv_tokens", "flare_free_snapshots", "flare_requests_failed_total")
def metrics():
text = urllib.request.urlopen("http://127.0.0.1:8000/metrics", timeout=10).read().decode()
return {k: float(m.group(1)) for k in KEYS for m in [re.search(rf"^{k} (\S+)$", text, re.M)] if m}
def ask(messages, max_tokens):
body = {"model": "Qwen3.8-27B", "messages": messages, "max_tokens": max_tokens, "temperature": 1.0}
req = urllib.request.Request("http://127.0.0.1:8000/v1/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=900).read()
return "ok"
except urllib.error.HTTPError as e:
return "fail: " + e.read().decode()[:120]
with open(out, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["i", "context", "prime", "warm", "seconds", *KEYS])
for i, (ctx, messages) in enumerate(order):
t = time.time()
prime, warm = ask(messages, 1), ask(messages, 64)
m = metrics()
w.writerow([i, ctx, prime, warm, round(time.time() - t, 1), *[m.get(k) for k in KEYS]]); f.flush()
print(i, ctx, prime[:40], warm[:40], m, flush=True)
if prime != "ok" or warm != "ok":
break
Narrowing
We collected these with temporary logging in Helios and a re-exported package. None of the logging is part of this issue.
Which check fails. We made the commit report a bitmask of its shape checks in place of a bare 6. The failing round reports proposed >= 1 and accepted == proposed - 1 as failed. Verification set proposed_count = 0, so it rejected the row (speculation.py L329, L355). The first round after the restore already fails at width 1, with no drafts. That leaves the target distribution as the cause: it is non-finite, and the drafts play no part.
What we ruled out.
- Page ownership. Before every submit we checked that each page id has exactly one owner: free list, checkpoint, retirement, request, retired request, inbound ticket or snapshot alias. There were no conflicts.
- Reuse while in flight. We tracked, per page, the last fence that used it as KV and as snapshot backing. No page was used in one role while work in the other role was still in flight.
- Alias range rounding. Widening every alias range by 8 pages on each side still fails.
- Snapshot exhaustion. Free snapshots stayed above 60 throughout.
- The row index. See the dedicated-pages rows in the table below.
What fixes it.
--memory-limit 50G, default MTP |
Result |
|---|---|
| As is | fails at prompt 15–17 |
| Snapshots 0–47 on dedicated pages, never handed to KV | no failures through the 25-minute cap (89 prompts) |
| Snapshots 53–100 on dedicated pages | no failures through the 25-minute cap (87 prompts) |
| Released alias pages zeroed before reuse | no failures in 143 of 143 prompts. Default memory also passes 143 of 143, where it failed at prompt 67 before |
With zeroing on, warm-request latency and benchmark throughput are unchanged. At 8k, N=1 to 32: 27.6 / 50.4 / 93.0 / 159 / 242 / 305 tok/s, against 28.2 / 49.0 / 92.8 / 154 / 244 / 308 tok/s without it.
Why only the tail page matters. _copy_pages (state_transfer.py L153) saves and restores the request's whole tail page, including positions past the endpoint. Startup zeroes every cache byte once (runtime_package/cache_memory.rs L478, "before any graph or transfer can read an alias"). After that, nothing clears a page when it goes from row storage back to the KV pool (scheduler/cache_memory.rs L153–165).
Potential fix
These are three options. The first is the one we tested.
- Zero pages leaving alias use. When
release_snapshotdrops a page's alias count to zero, queue the page. The executor then zeroes it on the model stream before the next graph launch, so the clear happens after the snapshot's last reader and before any KV writer. A single-GPU version of this is on a fork branch: shadowpa0327/flare-local-inference@b83d347a. It uses a global page list and host-to-device copies from pinned zeros, and doesn't cover the multi-process tensor-parallel executor. Treat it as a reference, not a proposed patch. - Clear only the dead tail. When a page is granted to a request, or a tail page is restored, zero the positions past the endpoint. This moves less data than option 1, but every writer of a partial page has to honor it.
- Make masked positions inert in attention. Skip or zero masked K/V positions in the paged kernels, instead of relying on
0 × valuestaying finite. This also protects any other path that leaves undefined bytes in a page.
Separately, as #61 also notes, a row the verifier rejects reaches the commit as INVALID_SPECULATION. Reporting it as INVALID_DISTRIBUTION would have pointed at the cause much sooner. The state-transfer validator also uses 6, for an unmapped page, which sent us looking at page tables first.
Found and written up with Claude Code (Claude Opus 5.5) while benchmarking Flare on DGX Spark.
- Ngôn ngữ chính
- Rust
- Star
- 800
- Fork
- 106
- Merge trung bình
- 1 ngày 3 giờ
- Pull request đã merge (30 ngày)
- 100
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của NVIDIA/hyperflare-inference
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
NVIDIA/hyperflare-inference#268 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
break out engram wait timeĐang mởarea:caching area:engram enhancement performance triage:design-review
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
NVIDIA/hyperflare-inference#230 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
NVIDIA/hyperflare-inference#278 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
NVIDIA/hyperflare-inference#276 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
NVIDIA/hyperflare-inference#271 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của NVIDIA/hyperflare-inference
Issue tương tự
-
bug UI
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
wardian-app/Wardian#1746 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
jlcodes99/cockpit-tools#3125 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
bug user-sprint
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Vondor/KassenSturz#967 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area/ci
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 62/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[BUG] Incremental sync warns "no stored highest_uid" on every round for folders with no new mailĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
rustmailer/bichon#411 ·