Skip to content

Commit d250b46

Browse files
howard0suCopilot
andcommitted
gemma4: full feature mirror resync after prefix cache restore
After restoring KV from a snapshot, do_prefill only syncs the feature mirror for the delta tokens [snap_pos..committed). The positions [0..snap_pos) in the mirror retain stale data from the previous request's decode phase (which may have diverged from the current prompt context after the ring buffer wraps). Fix: call draft_feature_mirror_sync_tail after restore to resync the entire [0..committed) feature range from cache_.target_feat to the mirror. This ensures the draft model sees consistent features and maintains high acceptance rate (AL) during speculative decoding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> gemma4: save/restore target_feat in prefix cache snapshot Matching Qwen35's approach: save target_feat (BF16 feature ring buffer) and last_tok as part of the KV snapshot. On restore, target_feat is copied back to GPU before the delta prefill + feature mirror resync. Previously, only K/V tensors were snapshotted. After restore, the feature mirror contained stale data from the previous request's decode phase, causing the draft model to make poor predictions and halving speculative decode acceptance rate (52% → 24%). With this fix, the full feature state is correctly restored, and the subsequent draft_feature_mirror_sync_tail ensures the mirror matches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1090d51 commit d250b46

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

dflash/src/gemma4/gemma4_backend.cpp

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,16 @@ GenerateResult Gemma4Backend::generate(const GenerateRequest & req,
511511
return result;
512512
}
513513

514+
// Inline snapshot at snap_pos for prefix cache
515+
if (req.snap_slot >= 0 && req.snap_pos > 0 && req.snap_pos <= committed) {
516+
cache_.cur_pos = req.snap_pos;
517+
if (snapshot_save(req.snap_slot)) {
518+
std::fprintf(stderr, "[gemma4] inline-snap slot=%d cur_pos=%d\n",
519+
req.snap_slot, req.snap_pos);
520+
}
521+
cache_.cur_pos = committed;
522+
}
523+
514524
if (req.n_gen > 0) {
515525
// Try speculative decode if draft is available and temp==0
516526
const bool can_spec = dflash_target_
@@ -623,8 +633,15 @@ GenerateResult Gemma4Backend::restore_and_generate(int slot,
623633
}
624634
}
625635

636+
// Restore target_feat from snapshot
637+
if (snap.feat_snap && cache_.target_feat) {
638+
const size_t feat_nbytes = ggml_nbytes(snap.feat_snap);
639+
ggml_backend_tensor_set(cache_.target_feat, snap.feat_snap->data, 0, feat_nbytes);
640+
}
641+
626642
const int snap_pos = snap.cur_pos;
627643
cache_.cur_pos = snap_pos;
644+
cache_.last_tok = snap.last_tok;
628645

629646
// Set up sampler
630647
sampler_ = req.sampler;
@@ -651,6 +668,24 @@ GenerateResult Gemma4Backend::restore_and_generate(int slot,
651668
}
652669
// else: prompt_len == snap_pos → no delta, committed stays at snap_pos
653670

671+
// Inline snapshot at snap_pos for prefix cache (new snap from this request)
672+
if (req.snap_slot >= 0 && req.snap_pos > 0 && req.snap_pos <= committed) {
673+
cache_.cur_pos = req.snap_pos;
674+
if (snapshot_save(req.snap_slot)) {
675+
std::fprintf(stderr, "[gemma4] inline-snap slot=%d cur_pos=%d\n",
676+
req.snap_slot, req.snap_pos);
677+
}
678+
cache_.cur_pos = committed;
679+
}
680+
681+
// Full feature mirror resync after restore: do_prefill only synced the
682+
// delta [snap_pos..committed). Re-sync the entire [0..committed) range so
683+
// the draft model sees correct features for the full context.
684+
if (feature_mirror_.target_feat && cache_.target_feat && !draft_parked_ && committed > 0) {
685+
draft_feature_mirror_sync_tail(cache_.target_feat, cache_.target_feat_cap,
686+
feature_mirror_, committed);
687+
}
688+
654689
// Generate
655690
if (req.n_gen > 0) {
656691
const bool can_spec = dflash_target_
@@ -738,8 +773,9 @@ bool Gemma4Backend::snapshot_save(int slot) {
738773
if (needs_alloc) {
739774
free_gemma4_snapshot(snap);
740775

776+
const int n_feat_tensors = (cache_.target_feat && cache_.target_feat_cap > 0) ? 1 : 0;
741777
ggml_init_params ip{};
742-
ip.mem_size = ggml_tensor_overhead() * (size_t)(n_layer * 2 + 4) + 4096;
778+
ip.mem_size = ggml_tensor_overhead() * (size_t)(n_layer * 2 + n_feat_tensors + 4) + 4096;
743779
ip.no_alloc = true;
744780
snap.ctx = ggml_init(ip);
745781
if (!snap.ctx) return false;
@@ -759,10 +795,21 @@ bool Gemma4Backend::snapshot_save(int slot) {
759795
}
760796
}
761797

798+
// target_feat: save min(snap_pos, target_feat_cap) positions
799+
snap.feat_snap = nullptr;
800+
snap.feat_cap = 0;
801+
if (cache_.target_feat && cache_.target_feat_cap > 0) {
802+
const int feat_len = std::min(snap_pos, cache_.target_feat_cap);
803+
snap.feat_snap = ggml_new_tensor_2d(snap.ctx, cache_.target_feat->type,
804+
cache_.target_feat->ne[0], feat_len);
805+
snap.feat_cap = cache_.target_feat_cap;
806+
}
807+
762808
snap.buf = ggml_backend_alloc_ctx_tensors(snap.ctx, snap_backend_);
763809
if (!snap.buf) {
764810
ggml_free(snap.ctx); snap.ctx = nullptr;
765811
snap.k_snap.clear(); snap.v_snap.clear();
812+
snap.feat_snap = nullptr;
766813
return false;
767814
}
768815
}
@@ -792,9 +839,15 @@ bool Gemma4Backend::snapshot_save(int slot) {
792839
}
793840
}
794841
snap.cur_pos = snap_pos;
842+
snap.last_tok = cache_.last_tok;
795843

796-
std::printf("[gemma4] snapshot saved slot=%d pos=%d\n", slot, snap.cur_pos);
797-
std::fflush(stdout);
844+
// target_feat: copy min(snap_pos, cap) positions from GPU to snapshot
845+
if (snap.feat_snap && cache_.target_feat) {
846+
const size_t feat_nbytes = ggml_nbytes(snap.feat_snap);
847+
ggml_backend_tensor_get(cache_.target_feat, snap.feat_snap->data, 0, feat_nbytes);
848+
}
849+
850+
std::fprintf(stderr, "[gemma4] snapshot saved slot=%d pos=%d\n", slot, snap.cur_pos);
798851
return true;
799852
}
800853

dflash/src/gemma4/gemma4_internal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,11 @@ bool create_gemma4_target_feat(ggml_backend_t backend, Gemma4Cache & cache,
193193
// Snapshot
194194
struct Gemma4Snapshot {
195195
int cur_pos = 0;
196+
int32_t last_tok = -1;
196197
std::vector<ggml_tensor *> k_snap;
197198
std::vector<ggml_tensor *> v_snap;
199+
ggml_tensor * feat_snap = nullptr; // [fc_in, feat_len]
200+
int feat_cap = 0;
198201
ggml_context * ctx = nullptr;
199202
ggml_backend_buffer_t buf = nullptr;
200203
};

dflash/src/gemma4/gemma4_loader.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,10 @@ void free_gemma4_snapshot(Gemma4Snapshot & s) {
478478
if (s.buf) { ggml_backend_buffer_free(s.buf); s.buf = nullptr; }
479479
if (s.ctx) { ggml_free(s.ctx); s.ctx = nullptr; }
480480
s.k_snap.clear(); s.v_snap.clear();
481-
s.cur_pos = 0;
481+
s.feat_snap = nullptr;
482+
s.feat_cap = 0;
483+
s.cur_pos = 0;
484+
s.last_tok = -1;
482485
}
483486

484487
} // namespace dflash27b

0 commit comments

Comments
 (0)