Skip to content

Commit ee1f89c

Browse files
committed
fix(dflash): stabilize long-prompt KVarN serving
Preserve the target model's absolute prompt position when DFlash flushes only a long-prompt prefill suffix into the cross-attention ring. This keeps drafter RoPE and masks aligned after full prompt reprocessing while retaining the bounded cross window. Bound SWA/hybrid prompt checkpoint memory by the prompt-cache RAM limit, prune checkpoints copied into prompt-cache entries, allow oversized cache entries to be fully evicted, and make explicit prompt clears release retained checkpoint buffers. Add regression coverage for absolute prefill commits, checkpoint-budget pruning, prompt clear ownership, and the server flush contract. Update the v0.3.2 changelog with the Qwen long-prompt garbage-output fix and Gemma follow-up memory/crash fix.
1 parent ef40457 commit ee1f89c

10 files changed

Lines changed: 240 additions & 42 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- Added experimental KVarN KV-cache compression as cache-type names: `--cache-type-k kvarn2|kvarn3|kvarn4` and `--cache-type-v kvarn2|kvarn3|kvarn4`. KVarN is kept to the target context, validates unsupported placements at runtime, uses non-unified CUDA streams, preserves stream-scoped backend state, and is wired for the current Qwen3.6 and Gemma4 paths.
99
- Hardened KVarN memory ownership and server rollback behavior. Full prompt-cache sequence clears are now allowed, arbitrary old suffix trims are rejected before mutation, composite memory preflights child removals before changing either side, and DFlash prompt-checkpoint reuse now skips or fully reprocesses prompts when the active memory cannot safely roll back.
1010
- Fixed the Gemma4 and Qwen3.6 DFlash + KVarN crash class seen after prompt-cache reuse. The server no longer asks KVarN-backed target memory to delete compressed historical groups during checkpoint restore, so repeated chat requests fall back to safe prefill instead of aborting in `seq_rm`.
11+
- Fixed long-prompt DFlash prefill after full prompt reprocessing. DFlash now keeps the drafter on the target model's absolute prompt timeline when flushing only the final cross-attention window, preventing invalid reduced-logit drafts and Qwen3.6 garbage output after long prompts.
12+
- Bounded prompt checkpoint memory for SWA/hybrid prompt-cache paths. Active checkpoints are byte-budgeted, prompt-cache saves prune or skip over-limit checkpoint state, cache eviction may remove every oversized entry, and explicit slot clears now release retained checkpoint buffers. This fixes the Gemma4 DFlash follow-up crash/memory spike where multi-GiB checkpoint lists were copied into prompt cache.
1113
- Reduced KVarN side effects outside KVarN runs. KVarN is disabled for draft and auxiliary contexts, iSWA now keeps the SWA side on the compact upstream cache instead of allocating a full-size KVarN SWA cache, and DFlash backup streams are gated by target architecture so Gemma recurrent paths do not allocate unused backup state.
1214
- Simplified the public KVarN surface while it is still experimental. The extra tuning flags were removed; the only user-facing selection is the cache type itself. Internal defaults such as sink tokens and Sinkhorn iterations now come from the selected `kvarnN` type.
1315
- Improved KVarN CUDA behavior and validation. Cross-stream state copies are explicitly ordered, CUDA materialization group counts are precomputed, `llama-bench` accepts KVarN cache-type names for benchmarking, and regression coverage now checks stream synchronization, sequence-removal capability, prompt-cache rollback safety, iSWA sizing policy, unsupported runtime policy, and argument parsing.

common/speculative.cpp

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,21 @@ common_dflash_ring_write common_dflash_ring_write_plan(int ring_size, int ring_p
241241
return { normalized_pos, ring_size, skip };
242242
}
243243

244+
int common_dflash_prefill_committed_after_flush(
245+
int current_committed,
246+
int n_written,
247+
int commit_end_pos) {
248+
if (n_written <= 0) {
249+
return current_committed;
250+
}
251+
252+
if (commit_end_pos > 0) {
253+
return commit_end_pos;
254+
}
255+
256+
return current_committed + n_written;
257+
}
258+
244259
static bool common_speculative_are_compatible(
245260
const llama_model * model_tgt,
246261
const llama_model * model_dft) {
@@ -369,7 +384,7 @@ struct common_speculative_impl {
369384

370385
virtual void update_logits_by_indices(llama_context * /*ctx*/, const std::vector<int> & /*capture_indices*/) {}
371386

372-
virtual int flush_prefill(int /*src_offset*/ = 0, int /*n_tokens*/ = 0) { return 0; }
387+
virtual int flush_prefill(int /*src_offset*/ = 0, int /*n_tokens*/ = 0, int /*commit_end_pos*/ = 0) { return 0; }
373388

374389
virtual int prepare_batch_draft(llama_context * /*ctx_dft_ext*/) { return -1; }
375390

@@ -2663,7 +2678,7 @@ struct common_speculative_impl_dflash : public common_speculative_impl {
26632678
return true;
26642679
}
26652680

2666-
int flush_prefill(int src_offset = 0, int n_tokens = 0) override {
2681+
int flush_prefill(int src_offset = 0, int n_tokens = 0, int commit_end_pos = 0) override {
26672682
llama_dflash_set_active_slot(ctx_tgt, seq_id);
26682683

26692684
prefill_flush_called = true;
@@ -2785,10 +2800,10 @@ struct common_speculative_impl_dflash : public common_speculative_impl {
27852800
? (writes_cpu_ring ? "cpu_ring+gpu_ring" : "gpu_ring")
27862801
: (writes_cpu_ring ? "cpu_ring" : "none");
27872802

2788-
LOG_INF("dflash prefill flush: capture_source=%s ring_dst=%s captured=%lld offset=%d n_tokens=%d to_write=%d n_src_layers=%d prefill_flushed=%d ring_write_pos=%d ring_filled=%d committed_len=%d\n",
2803+
LOG_INF("dflash prefill flush: capture_source=%s ring_dst=%s captured=%lld offset=%d n_tokens=%d to_write=%d n_src_layers=%d prefill_flushed=%d ring_write_pos=%d ring_filled=%d committed_len=%d commit_end_pos=%d\n",
27892804
capture_source, ring_dst, (long long) captured, offset, n_tokens,
27902805
to_write, n_src_layers, (int) prefill_flushed, ring_write_pos,
2791-
ring_filled, committed_len);
2806+
ring_filled, committed_len, commit_end_pos);
27922807
}
27932808

27942809
if (!prefill_flushed) {
@@ -2808,7 +2823,8 @@ struct common_speculative_impl_dflash : public common_speculative_impl {
28082823
return 0;
28092824
}
28102825

2811-
committed_len += actual_written;
2826+
committed_len = common_dflash_prefill_committed_after_flush(
2827+
committed_len, actual_written, commit_end_pos);
28122828
update_drafter_kv_cache(actual_written);
28132829
prefill_flushed = true;
28142830
prefill_flush_written += actual_written;
@@ -4849,13 +4865,13 @@ void common_speculative_rollback_dft(common_speculative * spec, llama_seq_id seq
48494865
}
48504866
}
48514867

4852-
int common_speculative_flush_prefill(common_speculative * spec, int src_offset, int n_tokens) {
4868+
int common_speculative_flush_prefill(common_speculative * spec, int src_offset, int n_tokens, int commit_end_pos) {
48534869
if (spec == nullptr) {
48544870
return 0;
48554871
}
48564872
int total_written = 0;
48574873
for (auto & impl : spec->impls) {
4858-
total_written += impl->flush_prefill(src_offset, n_tokens);
4874+
total_written += impl->flush_prefill(src_offset, n_tokens, commit_end_pos);
48594875
}
48604876
return total_written;
48614877
}

common/speculative.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ void common_speculative_print_stats(const common_speculative * spec);
114114

115115
common_dflash_ring_write common_dflash_ring_write_plan(int ring_size, int ring_pos, int n_tokens);
116116

117+
int common_dflash_prefill_committed_after_flush(
118+
int current_committed,
119+
int n_written,
120+
int commit_end_pos);
121+
117122
bool common_dflash_prefill_capture_complete_for_test(int captured, int requested);
118123

119124
bool common_dflash_cpu_ring_valid_after_write_for_test(
@@ -189,7 +194,7 @@ void common_speculative_draft_batch(
189194
void common_speculative_update_logits(common_speculative * spec, llama_context * ctx, const llama_tokens & batch_tokens, int n_accepted);
190195
void common_speculative_update_logits_deferred_dflash_kv(common_speculative * spec, llama_context * ctx, const llama_tokens & batch_tokens, int n_accepted);
191196
void common_speculative_update_logits_by_indices(common_speculative * spec, llama_context * ctx, const std::vector<int> & capture_indices);
192-
int common_speculative_flush_prefill(common_speculative * spec, int src_offset = 0, int n_tokens = 0);
197+
int common_speculative_flush_prefill(common_speculative * spec, int src_offset = 0, int n_tokens = 0, int commit_end_pos = 0);
193198
void common_speculative_set_prefill_capture_enabled(common_speculative * spec, bool enabled);
194199
void common_speculative_discard_dflash_state(common_speculative * spec, const char * reason);
195200
void common_speculative_note_prefill_suffix_scheduled(common_speculative * spec);

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ set_property(TEST test-kvarn PROPERTY LABELS main)
270270
llama_build_and_test(test-adaptive-dm.cpp)
271271
llama_build_and_test(test-server-prompt-checkpoint.cpp)
272272
target_include_directories(test-server-prompt-checkpoint PRIVATE ${PROJECT_SOURCE_DIR}/tools/server ${PROJECT_SOURCE_DIR}/tools/mtmd)
273+
target_link_libraries(test-server-prompt-checkpoint PRIVATE server-context)
273274
llama_build_and_test(test-dflash-ring.cpp)
274275
llama_build_and_test(test-cpu-out-prod.cpp)
275276
llama_build_and_test(test-cpu-fattn-support.cpp)

tests/test-dflash-plumbing.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,8 +1599,15 @@ int main(int argc, char ** argv) {
15991599
// Server should pass the planned source offset. GPU staging normalizes to
16001600
// window-relative offset 0 inside flush_prefill(), while CPU fallback needs
16011601
// the original sub-batch offset.
1602-
ok &= expect(server_context.find("common_speculative_flush_prefill(pf.spec, pf.span.src_offset,") != std::string::npos,
1603-
"server must pass planned source offset to prefill flush");
1602+
const size_t flush_call = server_context.find("common_speculative_flush_prefill(");
1603+
ok &= expect(flush_call != std::string::npos &&
1604+
server_context.find("pf.span.src_offset", flush_call) != std::string::npos &&
1605+
server_context.find("pf.span.n_tokens", flush_call) != std::string::npos &&
1606+
server_context.find("pf.span.capture_end", flush_call) != std::string::npos,
1607+
"server must pass planned source offset and absolute capture end to prefill flush");
1608+
ok &= expect(speculative_h.find("common_dflash_prefill_committed_after_flush") != std::string::npos &&
1609+
speculative.find("common_dflash_prefill_committed_after_flush(") != std::string::npos,
1610+
"DFlash prefill flush must preserve absolute committed positions for long prompts");
16041611

16051612
// Decode loop must compute intersection offsets
16061613
ok &= expect(context_cpp.find("inter_begin") != std::string::npos,

tests/test-dflash-ring.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,11 @@ int main() {
4646
assert(plan.src_token_offset == 0);
4747
}
4848

49+
{
50+
assert(common_dflash_prefill_committed_after_flush(0, 508, 54366) == 54366);
51+
assert(common_dflash_prefill_committed_after_flush(508, 512, 54878) == 54878);
52+
assert(common_dflash_prefill_committed_after_flush(1020, 4, 0) == 1024);
53+
}
54+
4955
return 0;
5056
}

tests/test-server-prompt-checkpoint.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,37 @@ int main() {
2222
assert(ckpt.ring_data.empty());
2323
assert(ckpt.ring_data.capacity() == 0);
2424

25+
server_prompt prompt;
26+
for (int i = 0; i < 3; ++i) {
27+
auto & cur = prompt.checkpoints.emplace_back();
28+
cur.n_tokens = i + 1;
29+
cur.data_tgt.resize((size_t) (i + 1) * 100);
30+
}
31+
32+
{
33+
server_prompt budgeted = server_prompt_clone_with_checkpoint_budget(prompt, 250, 600);
34+
assert(budgeted.checkpoints.size() == 1);
35+
assert(budgeted.checkpoints.front().n_tokens == 3);
36+
assert(server_prompt_checkpoints_size(budgeted.checkpoints) == 300);
37+
}
38+
39+
{
40+
server_prompt budgeted = server_prompt_clone_with_checkpoint_budget(prompt, 600, 600);
41+
assert(budgeted.checkpoints.empty());
42+
}
43+
44+
{
45+
server_prompt budgeted = server_prompt_clone_with_checkpoint_budget(prompt, 0, 0);
46+
assert(budgeted.checkpoints.size() == 3);
47+
assert(server_prompt_checkpoints_size(budgeted.checkpoints) == 600);
48+
}
49+
50+
prompt.data.main.resize(64);
51+
prompt.data.drft.resize(32);
52+
prompt.clear();
53+
assert(prompt.n_tokens() == 0);
54+
assert(prompt.data.size() == 0);
55+
assert(prompt.checkpoints.empty());
56+
2557
return 0;
2658
}

tools/server/server-context.cpp

Lines changed: 91 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ struct server_slot : server_adaptive_dm_state {
906906
common_context_seq_rm(ctx_dft, id, -1, -1);
907907
}
908908

909-
prompt.tokens.clear();
909+
prompt.clear();
910910
}
911911

912912
std::vector<common_adapter_lora_info> lora;
@@ -2224,6 +2224,28 @@ struct server_context_impl {
22242224
GGML_ABORT("failed to expand recurrent state after prompt cache restore; continuing would make scheduler reservation inconsistent\n");
22252225
}
22262226

2227+
size_t prompt_cache_limit_bytes() const {
2228+
return params_base.cache_ram_mib > 0
2229+
? (size_t) params_base.cache_ram_mib * 1024ull * 1024ull
2230+
: 0;
2231+
}
2232+
2233+
size_t context_checkpoint_budget_bytes() const {
2234+
const size_t cache_limit = prompt_cache_limit_bytes();
2235+
return cache_limit > 0 ? cache_limit / 2 : 0;
2236+
}
2237+
2238+
void erase_oldest_checkpoint(server_slot & slot, const char * reason) {
2239+
const auto & cur = slot.prompt.checkpoints.front();
2240+
2241+
SLT_WRN(slot,
2242+
"erasing old context checkpoint (%s, pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB)\n",
2243+
reason && reason[0] ? reason : "limit", cur.pos_min, cur.pos_max, cur.n_tokens,
2244+
(float) cur.size() / 1024 / 1024);
2245+
2246+
slot.prompt.checkpoints.erase(slot.prompt.checkpoints.begin());
2247+
}
2248+
22272249
bool dflash_shared_drafter_batch_allowed(int n_drafting) {
22282250
if (dflash_shared_drafter_batch_disabled()) {
22292251
return false;
@@ -4016,39 +4038,76 @@ struct server_context_impl {
40164038

40174039
// n_tokens_cur: the number of tokens added to the batch for the current slot
40184040
void create_checkpoint(server_slot & slot, const int64_t n_tokens_cur, llama_pos pos_min, llama_pos pos_max) {
4019-
while (slot.prompt.checkpoints.size() >= (size_t) params_base.n_ctx_checkpoints) {
4020-
// make room for the new checkpoint, if needed
4021-
const auto & cur = slot.prompt.checkpoints.front();
4041+
// Save DFlash ring buffer alongside the recurrent state checkpoint.
4042+
const bool dflash_checkpoint_slot =
4043+
slot.can_speculate() &&
4044+
params_base.speculative.type() == COMMON_SPECULATIVE_TYPE_DFLASH;
4045+
const size_t cur_size_tgt = ctx_tgt
4046+
? llama_state_seq_get_size_ext(ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY)
4047+
: 0;
4048+
const size_t cur_size_dft = ctx_dft
4049+
? llama_state_seq_get_size_ext(ctx_dft.get(), slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY)
4050+
: 0;
4051+
size_t ring_size = dflash_checkpoint_slot
4052+
? common_speculative_ring_state_size(slot.get_spec())
4053+
: 0;
4054+
const size_t new_size = cur_size_tgt + cur_size_dft + ring_size;
4055+
const size_t checkpoint_budget = context_checkpoint_budget_bytes();
40224056

4023-
SLT_WRN(slot, "erasing old context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB)\n",
4024-
cur.pos_min, cur.pos_max, cur.n_tokens, (float) cur.size() / 1024 / 1024);
4057+
if (checkpoint_budget > 0 && new_size > checkpoint_budget) {
4058+
SLT_WRN(slot,
4059+
"skipping context checkpoint: state size %.3f MiB exceeds checkpoint budget %.3f MiB\n",
4060+
new_size / (1024.0 * 1024.0), checkpoint_budget / (1024.0 * 1024.0));
4061+
return;
4062+
}
40254063

4026-
slot.prompt.checkpoints.erase(slot.prompt.checkpoints.begin());
4064+
while (slot.prompt.checkpoints.size() >= (size_t) params_base.n_ctx_checkpoints) {
4065+
erase_oldest_checkpoint(slot, "count limit");
40274066
}
40284067

4029-
auto & cur = slot.prompt.checkpoints.emplace_back();
4068+
while (checkpoint_budget > 0 &&
4069+
!slot.prompt.checkpoints.empty() &&
4070+
server_prompt_checkpoints_size(slot.prompt.checkpoints) + new_size > checkpoint_budget) {
4071+
erase_oldest_checkpoint(slot, "RAM budget");
4072+
}
40304073

4074+
common_prompt_checkpoint cur;
40314075
cur.update_pos(slot.prompt.n_tokens() - n_tokens_cur, pos_min, pos_max);
40324076

4033-
cur.update_tgt(ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
4034-
cur.update_dft(ctx_dft.get(), slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
4077+
try {
4078+
cur.data_tgt.resize(cur_size_tgt);
4079+
cur.data_dft.resize(cur_size_dft);
4080+
cur.ring_data.resize(ring_size);
4081+
} catch (const std::bad_alloc & e) {
4082+
SLT_WRN(slot,
4083+
"skipping context checkpoint: failed to allocate %.3f MiB state (%s)\n",
4084+
new_size / (1024.0 * 1024.0), e.what());
4085+
return;
4086+
}
4087+
4088+
if (cur_size_tgt > 0) {
4089+
const size_t n = llama_state_seq_get_data_ext(
4090+
ctx_tgt, cur.data_tgt.data(), cur_size_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
4091+
if (n != cur_size_tgt) {
4092+
GGML_ABORT("checkpoint size mismatch: expected %zu, got %zu\n", cur_size_tgt, n);
4093+
}
4094+
}
4095+
4096+
if (ctx_dft && cur_size_dft > 0) {
4097+
const size_t n = llama_state_seq_get_data_ext(
4098+
ctx_dft.get(), cur.data_dft.data(), cur_size_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
4099+
if (n != cur_size_dft) {
4100+
GGML_ABORT("checkpoint size mismatch: expected %zu, got %zu\n", cur_size_dft, n);
4101+
}
4102+
}
40354103

4036-
// Save DFlash ring buffer alongside the recurrent state checkpoint.
4037-
const bool dflash_checkpoint_slot =
4038-
slot.can_speculate() &&
4039-
params_base.speculative.type() == COMMON_SPECULATIVE_TYPE_DFLASH;
4040-
size_t ring_size = 0;
40414104
bool ring_saved = false;
4042-
if (dflash_checkpoint_slot) {
4043-
ring_size = common_speculative_ring_state_size(slot.get_spec());
4044-
if (ring_size > 0) {
4045-
cur.ring_data.resize(ring_size);
4046-
ring_saved = common_speculative_ring_state_save(
4047-
slot.get_spec(), cur.ring_data.data(), ring_size);
4048-
if (!ring_saved) {
4049-
cur.ring_data.clear();
4050-
ring_size = 0;
4051-
}
4105+
if (dflash_checkpoint_slot && ring_size > 0) {
4106+
ring_saved = common_speculative_ring_state_save(
4107+
slot.get_spec(), cur.ring_data.data(), ring_size);
4108+
if (!ring_saved) {
4109+
cur.ring_data.clear();
4110+
ring_size = 0;
40524111
}
40534112
}
40544113
if (dflash_checkpoint_slot &&
@@ -4059,10 +4118,13 @@ struct server_context_impl {
40594118
cur.pos_min, cur.pos_max, cur.n_tokens, ring_size, ring_saved ? 1 : 0);
40604119
}
40614120

4121+
slot.prompt.checkpoints.push_back(std::move(cur));
4122+
const auto & saved = slot.prompt.checkpoints.back();
4123+
40624124
SLT_INF(slot,
40634125
"created context checkpoint %d of %d (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB)\n",
4064-
(int) slot.prompt.checkpoints.size(), params_base.n_ctx_checkpoints, cur.pos_min,
4065-
cur.pos_max, cur.n_tokens, (float) cur.size() / 1024 / 1024);
4126+
(int) slot.prompt.checkpoints.size(), params_base.n_ctx_checkpoints, saved.pos_min,
4127+
saved.pos_max, saved.n_tokens, (float) saved.size() / 1024 / 1024);
40664128
}
40674129

40684130
void process_single_task(server_task && task) {
@@ -6362,7 +6424,8 @@ struct server_context_impl {
63626424
// DFlash: flush captured hidden states into the ring buffer before
63636425
// the next llama_decode resets the capture buffer.
63646426
for (auto & pf : pending_prefill_flushes) {
6365-
const int written = common_speculative_flush_prefill(pf.spec, pf.span.src_offset, pf.span.n_tokens);
6427+
const int written = common_speculative_flush_prefill(
6428+
pf.spec, pf.span.src_offset, pf.span.n_tokens, pf.span.capture_end);
63666429
if (written != pf.span.n_tokens) {
63676430
SRV_ERR("dflash prefill flush mismatch: slot=%d requested=%d written=%d src_offset=%d; disabling DFlash drafting until fresh hiddens are available\n",
63686431
pf.slot_id, pf.span.n_tokens, written, pf.span.src_offset);

0 commit comments

Comments
 (0)