@@ -62,6 +62,7 @@ Qwen35DFlashTarget::~Qwen35DFlashTarget() {
6262 vprof_sum_get_hpre_, vprof_sum_get_argmax_, vprof_sum_total_);
6363 }
6464 step_graph_destroy (proj_sg_);
65+ if (rollback_stream_) { cudaStreamDestroy (rollback_stream_); rollback_stream_ = nullptr ; }
6566}
6667
6768Qwen35DFlashTarget::Qwen35DFlashTarget (
@@ -264,27 +265,9 @@ bool Qwen35DFlashTarget::verify_batch(
264265
265266 cache_.cur_pos = base_pos + n_tokens;
266267
267- // Record linear-chain "tree" topology so restore_kv_at_chain (and
268- // restore_kv_at_dfs, for completeness) can roll back to any accepted
269- // prefix of this batch. Only meaningful when capture actually fired —
270- // when it didn't, leave last_tree_base_pos_ at its prior value (could be
271- // -1 or a stale verify_tree value); the chain runner only calls
272- // restore_kv_at_chain on iters where it itself enabled capture, and a
273- // stale last_tree_* wouldn't be consulted by restore_kv_at_dfs in that
274- // window because verify_batch resets it here on every captured call.
275- if (capture_intermediate) {
276- last_tree_base_pos_ = base_pos;
277- last_tree_n_nodes_ = n_tokens - 1 ;
278- last_tree_parents_.resize (n_tokens);
279- last_tree_depths_.resize (n_tokens > 0 ? (size_t )(n_tokens - 1 ) : 0 );
280- if (n_tokens > 0 ) last_tree_parents_[0 ] = -1 ; // root marker
281- for (int i = 1 ; i < n_tokens; i++) {
282- last_tree_parents_[i] = i - 1 ;
283- last_tree_depths_[i - 1 ] = i;
284- }
285- } else {
286- // Invalidate so a stray restore_kv_at_chain() returns false instead of
287- // rolling back against stale capture buffers from an earlier iter.
268+ // Topology is owned by the caller via capture_topology_for_chain(); if
269+ // capture fired without prior topology, invalidate to be defensive.
270+ if (!capture_intermediate) {
288271 last_tree_base_pos_ = -1 ;
289272 }
290273
@@ -459,7 +442,14 @@ bool Qwen35DFlashTarget::restore_kv_at_dfs(const std::vector<int> & accepted_dfs
459442 }
460443
461444 const int n_delta = (int )cache_.ssm_intermediate .size ();
462- cudaStream_t stream = nullptr ; // default stream — serializes with next graph_compute
445+ // Bug #3: dedicated stream so the rollback copies don't serialize with
446+ // the default stream (e.g. ggml backend compute, host syncs).
447+ if (!rollback_stream_) {
448+ if (cudaStreamCreate (&rollback_stream_) != cudaSuccess) {
449+ rollback_stream_ = nullptr ; // fall back to default
450+ }
451+ }
452+ cudaStream_t stream = rollback_stream_;
463453 for (int il = 0 ; il < n_delta; il++) {
464454 ggml_tensor * ssm_inter = cache_.ssm_intermediate [il];
465455 ggml_tensor * conv_in = cache_.conv_input_cache [il];
@@ -533,9 +523,9 @@ bool Qwen35DFlashTarget::restore_kv_at_dfs(const std::vector<int> & accepted_dfs
533523 }
534524
535525 // Full-attention KV compaction: verify_tree wrote K/V at slots
536- // [base..base+N-1] in DFS order. For the next iter's verify to see the
537- // correct committed prefix, slots [base..base+commit_n-1] must hold the
538- // K/V of the accepted path. d==0 (root) is trivially aligned .
526+ // [base..base+N-1] in DFS order. Bug #3: collapse the per-head inner
527+ // loop into one cudaMemcpy2DAsync (pitch=nb[2], height=n_kv) — saves
528+ // 2*n_kv-2 launches per (layer, d) pair on a dedicated stream .
539529 if (walked_sibling) {
540530 const int base = last_tree_base_pos_;
541531 const int n_full_attn = (int )cache_.attn_k .size ();
@@ -548,30 +538,46 @@ bool Qwen35DFlashTarget::restore_kv_at_dfs(const std::vector<int> & accepted_dfs
548538 ggml_tensor * cv = cache_.attn_v [l];
549539 if (!ck || !cv) continue ;
550540 const size_t slot_bytes = ck->nb [1 ];
551- const size_t src_off = (size_t )(base + src_dfs) * slot_bytes;
552- const size_t dst_off = (size_t )(base + dst_slot) * slot_bytes;
553- const int n_kv = (int )ck->ne [2 ];
554- for (int h = 0 ; h < n_kv; h++) {
555- const size_t head_src = src_off + (size_t )h * ck->nb [2 ];
556- const size_t head_dst = dst_off + (size_t )h * ck->nb [2 ];
557- cudaMemcpyAsync ((char *)ck->data + head_dst,
558- (const char *)ck->data + head_src,
559- slot_bytes, cudaMemcpyDeviceToDevice, stream);
560- cudaMemcpyAsync ((char *)cv->data + head_dst,
561- (const char *)cv->data + head_src,
562- slot_bytes, cudaMemcpyDeviceToDevice, stream);
563- }
541+ const int n_kv = (int )ck->ne [2 ];
542+ const size_t pitch = ck->nb [2 ];
543+ const size_t src_off = (size_t )(base + src_dfs) * slot_bytes;
544+ const size_t dst_off = (size_t )(base + dst_slot) * slot_bytes;
545+ cudaMemcpy2DAsync ((char *)ck->data + dst_off, pitch,
546+ (const char *)ck->data + src_off, pitch,
547+ slot_bytes, n_kv,
548+ cudaMemcpyDeviceToDevice, stream);
549+ cudaMemcpy2DAsync ((char *)cv->data + dst_off, cv->nb [2 ],
550+ (const char *)cv->data + src_off, cv->nb [2 ],
551+ slot_bytes, (int )cv->ne [2 ],
552+ cudaMemcpyDeviceToDevice, stream);
564553 }
565554 }
566555 }
567556
557+ // Sync rollback stream so the next graph_compute (on default stream)
558+ // sees a consistent KV/SSM state.
559+ if (rollback_stream_) cudaStreamSynchronize (rollback_stream_);
560+
568561 // Advance cur_pos to "just past the last committed slot" so the next
569562 // verify_batch's kv_start lines up. root = dfs 0 lives at base, so
570563 // commit_n committed tokens occupy slots [base..base+commit_n-1].
571564 cache_.cur_pos = last_tree_base_pos_ + commit_n;
572565 return true ;
573566}
574567
568+ void Qwen35DFlashTarget::capture_topology_for_chain (int n_tokens, int base_pos) {
569+ if (n_tokens <= 0 ) { last_tree_base_pos_ = -1 ; return ; }
570+ last_tree_base_pos_ = base_pos;
571+ last_tree_n_nodes_ = n_tokens - 1 ;
572+ last_tree_parents_.resize (n_tokens);
573+ last_tree_depths_.resize ((size_t )(n_tokens - 1 ));
574+ last_tree_parents_[0 ] = -1 ;
575+ for (int i = 1 ; i < n_tokens; i++) {
576+ last_tree_parents_[i] = i - 1 ;
577+ last_tree_depths_[i - 1 ] = i;
578+ }
579+ }
580+
575581bool Qwen35DFlashTarget::restore_kv_at_chain (int accept_n) {
576582 // A chain of N tokens recorded by verify_batch is the DFS spine
577583 // [0, 1, ..., N-1]. Roll back to slot accept_n: the first (accept_n + 1)
0 commit comments