Skip to content

Commit 66d71cc

Browse files
AmidwestnoobDrew Schuyler
andauthored
perf(qwen35): add opt-in F32 rollback checkpoints for single-GPU inference (#515)
* perf(qwen35): add opt-in F32 rollback checkpoints * fix(qwen35): address rollback policy review findings * refactor(qwen35): centralize rollback diagnostics * fix(qwen35): guard null stream in rollback diagnostics Treat a null diagnostics stream as a no-op before calling fprintf, and cover the diagnostics-enabled null-stream path in the rollback policy test. Addresses the latest Cubic P3 on PR #515. --------- Co-authored-by: Drew Schuyler <[email protected]>
1 parent 9b730ee commit 66d71cc

11 files changed

Lines changed: 352 additions & 40 deletions

server/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,12 @@ if(DFLASH27B_TESTS)
741741
target_include_directories(test_derived_scalars PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS})
742742
add_test(NAME derived_scalars COMMAND test_derived_scalars)
743743
endif()
744+
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_chain_rollback_policy.cpp")
745+
add_executable(test_chain_rollback_policy test/test_chain_rollback_policy.cpp)
746+
target_include_directories(test_chain_rollback_policy PRIVATE
747+
${CMAKE_CURRENT_SOURCE_DIR}/src/common)
748+
add_test(NAME chain_rollback_policy COMMAND test_chain_rollback_policy)
749+
endif()
744750
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_kvflash_placement.cpp")
745751
add_executable(test_kvflash_placement test/test_kvflash_placement.cpp)
746752
target_include_directories(test_kvflash_placement PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS})
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <cstdio>
5+
#include <cstdlib>
6+
#include <cstring>
7+
8+
namespace dflash::common {
9+
10+
struct ChainRollbackPolicy {
11+
bool checkpoint_f32 = false;
12+
int fast_rollback_threshold = 5;
13+
bool diagnostics = false;
14+
};
15+
16+
inline bool env_flag_enabled(const char * name) {
17+
const char * value = std::getenv(name);
18+
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
19+
}
20+
21+
inline ChainRollbackPolicy resolve_chain_rollback_policy() {
22+
ChainRollbackPolicy policy;
23+
policy.checkpoint_f32 = env_flag_enabled("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32");
24+
policy.diagnostics = env_flag_enabled("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG");
25+
26+
// Lower thresholds are valid only with exact F32 checkpoints. This keeps
27+
// the established F16 behavior unchanged when no opt-in flags are set.
28+
if (policy.checkpoint_f32) {
29+
const char * value = std::getenv("DFLASH_FAST_ROLLBACK_THRESHOLD");
30+
if (value != nullptr) {
31+
const int requested = std::atoi(value);
32+
if (requested >= 1 && requested <= 5) {
33+
policy.fast_rollback_threshold = requested;
34+
}
35+
}
36+
}
37+
return policy;
38+
}
39+
40+
// Rollback diagnostics shared by the two spec-decode loops
41+
// (Qwen35Backend::do_spec_decode and run_dflash_spec_decode). Keeping the
42+
// counters and the print format in one place prevents the loops from
43+
// drifting when the diagnostics change.
44+
struct RollbackDiag {
45+
int accept_hist[17] = {};
46+
int fast_low = 0; // fast rollbacks below the F16 breakeven (accept_n < 5)
47+
int fast_high = 0; // fast rollbacks at accept_n >= 5
48+
int legacy_replay = 0;
49+
int failed_fallback = 0;
50+
51+
void record_accept(int accept_n) {
52+
accept_hist[std::min(accept_n, 16)]++;
53+
}
54+
void record_fast_rollback(int accept_n) {
55+
if (accept_n < 5) fast_low++;
56+
else fast_high++;
57+
}
58+
void record_failed_fallback() { failed_fallback++; }
59+
void record_legacy_replay() { legacy_replay++; }
60+
61+
void print(const ChainRollbackPolicy & policy, std::FILE * out) const {
62+
if (!out || !policy.diagnostics) return;
63+
std::fprintf(out,
64+
"[chain-rollback-policy] checkpoint=%s threshold=%d fast_low=%d fast_high=%d legacy_replay=%d failed_fallback=%d accept_hist=1:%d,2:%d,3:%d,4:%d,5:%d,6:%d,7:%d,8:%d,9:%d,10:%d,11:%d,12:%d,13:%d,14:%d,15:%d,16+:%d\n",
65+
policy.checkpoint_f32 ? "F32" : "default",
66+
policy.fast_rollback_threshold,
67+
fast_low, fast_high, legacy_replay, failed_fallback,
68+
accept_hist[1], accept_hist[2], accept_hist[3],
69+
accept_hist[4], accept_hist[5], accept_hist[6],
70+
accept_hist[7], accept_hist[8], accept_hist[9],
71+
accept_hist[10], accept_hist[11], accept_hist[12],
72+
accept_hist[13], accept_hist[14], accept_hist[15],
73+
accept_hist[16]);
74+
}
75+
};
76+
77+
} // namespace dflash::common

server/src/common/dflash_spec_decode.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <cstdio>
1313
#include <vector>
1414

15+
#include "chain_rollback_policy.h"
16+
1517
namespace dflash::common {
1618

1719
namespace {
@@ -84,6 +86,8 @@ bool run_dflash_spec_decode(
8486
int n_accept_sum = 0;
8587
int n_hint_proposed = 0;
8688
int n_hint_accepted = 0;
89+
const ChainRollbackPolicy rollback_policy = resolve_chain_rollback_policy();
90+
RollbackDiag rollback_diag;
8791

8892
auto t_dec0 = std::chrono::steady_clock::now();
8993
while (n_generated < n_gen) {
@@ -213,9 +217,10 @@ bool run_dflash_spec_decode(
213217

214218
// ── Commit accepted tokens to KV state ──────────────────────────
215219
// Adaptive: use fast-rollback when acceptance is high enough to benefit.
216-
constexpr int kFastRollbackThreshold = 5;
220+
rollback_diag.record_accept(accept_n);
217221
const bool use_fast_rollback =
218-
target.supports_fast_rollback() && (accept_n >= kFastRollbackThreshold);
222+
target.supports_fast_rollback() &&
223+
(accept_n >= rollback_policy.fast_rollback_threshold);
219224

220225
std::vector<int32_t> replay_tok((size_t)commit_n);
221226
for (int i = 0; i < commit_n; i++) {
@@ -236,15 +241,18 @@ bool run_dflash_spec_decode(
236241
if (target.rollback_to(committed, commit_n)) {
237242
last_tok = target_tok[commit_n - 1];
238243
fast_rolled_back = true;
244+
rollback_diag.record_fast_rollback(accept_n);
239245
} else {
240246
// Rollback failed (e.g. CUDA error / unsupported state type).
241247
// The pre-verify snapshot is still valid, so degrade to the
242248
// legacy restore+replay path below instead of aborting.
243249
std::fprintf(stderr, "dflash-spec rollback_to failed; "
244250
"falling back to restore+replay\n");
251+
rollback_diag.record_failed_fallback();
245252
}
246253
}
247254
if (!fast_rolled_back) {
255+
rollback_diag.record_legacy_replay();
248256
// Legacy path: restore SSM snapshot and replay accepted + bonus tokens.
249257
// (When falling back from fast-rollback, bonus_tok is already -1 and
250258
// replay_tok/commit_n reflect the budget-clamped accepted set.)
@@ -296,6 +304,7 @@ bool run_dflash_spec_decode(
296304
std::printf("[target-split-dflash] %d draft steps, accepted=%d/%d (%.1f%%), avg commit/step=%.2f\n",
297305
n_draft_steps, n_accept_sum, total_draft_pos, accept_pct,
298306
n_draft_steps > 0 ? (double)n_generated / (double)n_draft_steps : 0.0);
307+
rollback_diag.print(rollback_policy, stdout);
299308
if (n_hint_proposed > 0) {
300309
std::printf("[target-split-dflash] hint tokens: %d/%d accepted (%.1f%%)\n",
301310
n_hint_accepted, n_hint_proposed,

server/src/internal.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,8 @@ ggml_tensor * build_qwen35_layer(
654654
int fa_window = 0,
655655
ggml_tensor * q_tail_capture = nullptr,
656656
int q_tail_start = 0,
657-
ggml_tensor * kv_write_rows = nullptr);
657+
ggml_tensor * kv_write_rows = nullptr,
658+
ggml_tensor * parent_ids = nullptr);
658659

659660
// Overload that also exposes the MoE router selection tensor (if MoE layer).
660661
ggml_tensor * build_qwen35_layer(
@@ -673,7 +674,8 @@ ggml_tensor * build_qwen35_layer(
673674
ggml_tensor * q_tail_capture,
674675
int q_tail_start,
675676
ggml_tensor ** moe_selected_out,
676-
ggml_tensor * kv_write_rows = nullptr);
677+
ggml_tensor * kv_write_rows = nullptr,
678+
ggml_tensor * parent_ids = nullptr);
677679

678680
QwenLayerPrefnOutputs build_qwen35_layer_prefn(
679681
ggml_context * ctx,

server/src/qwen35/graph_builders.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ bool build_layer_step(
2424
bool capture,
2525
int fa_window,
2626
int kq_stride_pad,
27-
bool kvflash) {
27+
bool kvflash,
28+
bool tree_mode) {
2829
if (kvflash) with_mask = true;
2930
step_graph_free(sg);
3031

@@ -74,14 +75,20 @@ bool build_layer_step(
7475
}
7576
}
7677

78+
if (tree_mode && !is_attn) {
79+
sg.parent_ids = ggml_new_tensor_1d(sg.ctx, GGML_TYPE_I32, n_tokens);
80+
ggml_set_name(sg.parent_ids, "parent_ids");
81+
ggml_set_input(sg.parent_ids);
82+
}
83+
7784
sg.gf = ggml_new_graph_custom(sg.ctx, 16384, false);
7885

7986
ggml_tensor * layer_out = build_qwen35_layer(
8087
sg.ctx, sg.gf, w, cache, layer_idx,
8188
sg.inp_embed, sg.positions, sg.attn_mask,
8289
kv_start, n_tokens, capture, fa_window,
8390
/*q_tail_capture=*/nullptr, /*q_tail_start=*/0,
84-
sg.kv_write_rows);
91+
sg.kv_write_rows, sg.parent_ids);
8592
if (!layer_out) return false;
8693

8794
ggml_tensor * out_view = ggml_view_2d(sg.ctx, act_out,

server/src/qwen35/graph_builders.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ bool build_layer_step(
3939
bool capture,
4040
int fa_window = 0,
4141
int kq_stride_pad = KQ_MASK_PAD,
42-
bool kvflash = false);
42+
bool kvflash = false,
43+
bool tree_mode = false);
4344

4445
// `kvflash`: pooled mode — KV rows go through a set_rows input
4546
// (sg.kv_write_rows, [n_tokens, n_head_kv] ne0-major slots) and the mask

server/src/qwen35/qwen35_backend.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "qwen35_backend.h"
2+
#include "common/chain_rollback_policy.h"
23
#include "placement/skip_park_guard.h"
34
#include "qwen35_dflash_target.h"
45
#include "graph_builders.h"
@@ -1927,12 +1928,15 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
19271928
int n_hint_proposed = 0;
19281929
int n_hint_accepted = 0;
19291930
int target_forwards = 0;
1931+
const ChainRollbackPolicy rollback_policy = resolve_chain_rollback_policy();
1932+
RollbackDiag rollback_diag;
19301933

19311934
auto log_target_forward_stats = [&]() {
19321935
std::fprintf(stderr, "[spec-decode] target_forwards=%d forwards_per_token=%.6f forwards_per_step=%.3f\n",
19331936
target_forwards,
19341937
n_generated > 0 ? (double)target_forwards / n_generated : 0.0,
19351938
n_draft_steps > 0 ? (double)target_forwards / n_draft_steps : 0.0);
1939+
rollback_diag.print(rollback_policy, stderr);
19361940
};
19371941

19381942
// kvflash: an in-pool prompt prefills contiguously without registering
@@ -2488,11 +2492,12 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
24882492
// 6. Fix state: adaptive fast-rollback vs legacy replay.
24892493
// Fast-rollback (implicit bonus, skip replay) is profitable when
24902494
// accept_n is large enough that skipping the replay saves more compute
2491-
// than the cost of deferring the bonus to the next step. Breakeven
2492-
// is around accept_n ≈ 5. Below that, legacy replay is cheaper.
2493-
constexpr int kFastRollbackThreshold = 5;
2495+
// than the cost of deferring the bonus to the next step. The default
2496+
// threshold is 5; exact F32 checkpoints may opt in to a lower value.
2497+
rollback_diag.record_accept(accept_n);
24942498
const bool use_fast_rollback =
2495-
target->supports_fast_rollback() && (accept_n >= kFastRollbackThreshold);
2499+
target->supports_fast_rollback() &&
2500+
(accept_n >= rollback_policy.fast_rollback_threshold);
24962501

24972502
int replay_last_tok = -1;
24982503
bool fast_rolled_back = false;
@@ -2508,15 +2513,18 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
25082513
if (target->rollback_to(committed, commit_n)) {
25092514
replay_last_tok = target_tok[commit_n - 1];
25102515
fast_rolled_back = true;
2516+
rollback_diag.record_fast_rollback(accept_n);
25112517
} else {
25122518
// Rollback failed (CUDA error / unsupported state type). The
25132519
// pre-verify snapshot is still valid, so degrade to the legacy
25142520
// restore+replay path below instead of aborting the request.
25152521
std::fprintf(stderr, "spec-decode: rollback_to failed; "
25162522
"falling back to restore+replay\n");
2523+
rollback_diag.record_failed_fallback();
25172524
}
25182525
}
25192526
if (!fast_rolled_back) {
2527+
rollback_diag.record_legacy_replay();
25202528
// Legacy replay: restore SSM snapshot, replay accepted + bonus tokens.
25212529
// (When falling back from fast-rollback, bonus_tok is -1 and commit_n
25222530
// is the budget-clamped accepted count.)

server/src/qwen35/qwen35_dflash_target.cpp

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,24 @@ bool Qwen35DFlashTarget::rollback_to_tree(
376376
(size_t)rollback_dfs * cap.ssm_intermediate_states->nb[3];
377377
const void * ssm_src =
378378
(const char *)cap.ssm_intermediate_states->data + ssm_src_offset;
379-
const auto to_fp32 = ggml_get_to_fp32_cuda(cap.ssm_intermediate_states->type);
380-
if (!to_fp32) {
381-
std::fprintf(stderr, "rollback_to_tree: no fp32 converter for type %d (layer %d)\n",
382-
(int)cap.ssm_intermediate_states->type, il);
383-
return false;
379+
if (cap.ssm_intermediate_states->type == GGML_TYPE_F32) {
380+
const cudaError_t ce = cudaMemcpyAsync(cache_.ssm_state[il]->data, ssm_src,
381+
ssm_elems * sizeof(float),
382+
cudaMemcpyDeviceToDevice, stream);
383+
if (ce != cudaSuccess) {
384+
std::fprintf(stderr, "rollback_to_tree: F32 SSM copy failed at layer %d: %s\n",
385+
il, cudaGetErrorString(ce));
386+
return false;
387+
}
388+
} else {
389+
const auto to_fp32 = ggml_get_to_fp32_cuda(cap.ssm_intermediate_states->type);
390+
if (!to_fp32) {
391+
std::fprintf(stderr, "rollback_to_tree: no fp32 converter for type %d (layer %d)\n",
392+
(int)cap.ssm_intermediate_states->type, il);
393+
return false;
394+
}
395+
to_fp32(ssm_src, (float *)cache_.ssm_state[il]->data, (int64_t)ssm_elems, stream);
384396
}
385-
to_fp32(ssm_src, (float *)cache_.ssm_state[il]->data, (int64_t)ssm_elems, stream);
386397

387398
// Conv state ← the K-1 most recent inputs along rollback_dfs's ancestry.
388399
const int K_conv = 4;
@@ -551,7 +562,6 @@ bool Qwen35DFlashTarget::rollback_to(int base_pos, int commit_n) {
551562
cache_.cur_pos = base_pos + commit_n;
552563
return true;
553564
}
554-
555565
const int rollback_idx = commit_n - 1; // index into per-step intermediates
556566
cudaStream_t stream = nullptr;
557567

@@ -589,16 +599,30 @@ bool Qwen35DFlashTarget::rollback_to(int base_pos, int commit_n) {
589599
(size_t)rollback_idx * cap.ssm_intermediate_states->nb[3];
590600
const void * ssm_src =
591601
(const char *)cap.ssm_intermediate_states->data + ssm_src_offset;
592-
const auto to_fp32 = ggml_get_to_fp32_cuda(cap.ssm_intermediate_states->type);
593-
if (!to_fp32) {
594-
if (kFastRollbackDiag) {
595-
std::fprintf(stderr, "rollback_to: no fp32 converter type=%d layer=%d\n",
596-
(int)cap.ssm_intermediate_states->type, il);
602+
if (cap.ssm_intermediate_states->type == GGML_TYPE_F32) {
603+
const size_t ssm_bytes = ssm_elems * sizeof(float);
604+
const cudaError_t ce = cudaMemcpyAsync(cache_.ssm_state[il]->data, ssm_src,
605+
ssm_bytes,
606+
cudaMemcpyDeviceToDevice, stream);
607+
if (ce != cudaSuccess) {
608+
if (kFastRollbackDiag) {
609+
std::fprintf(stderr, "rollback_to: F32 SSM copy failed layer=%d: %s\n",
610+
il, cudaGetErrorString(ce));
611+
}
612+
return false;
597613
}
598-
return false;
614+
} else {
615+
const auto to_fp32 = ggml_get_to_fp32_cuda(cap.ssm_intermediate_states->type);
616+
if (!to_fp32) {
617+
if (kFastRollbackDiag) {
618+
std::fprintf(stderr, "rollback_to: no fp32 converter type=%d layer=%d\n",
619+
(int)cap.ssm_intermediate_states->type, il);
620+
}
621+
return false;
622+
}
623+
to_fp32(ssm_src, (float *)cache_.ssm_state[il]->data,
624+
(int64_t)ssm_elems, stream);
599625
}
600-
to_fp32(ssm_src, (float *)cache_.ssm_state[il]->data,
601-
(int64_t)ssm_elems, stream);
602626

603627
// Conv rollback: copy conv_input[commit_n..commit_n+K-2, :, :]
604628
// into cache.conv_state[il].

0 commit comments

Comments
 (0)