Skip to content

Commit cf21aa1

Browse files
authored
Merge pull request #379 from cheese-cakee/fix/budget-hook-spec-decode
fix(qwen35): keep spec-decode active through BudgetHook close boundary
2 parents ff27267 + 8b2973c commit cf21aa1

1 file changed

Lines changed: 74 additions & 41 deletions

File tree

server/src/qwen35/qwen35_backend.cpp

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,47 +1416,10 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
14161416
while (n_generated < n_gen) {
14171417
const int need_commit_budget = n_gen - n_generated;
14181418

1419-
// Budget tail-off: when remaining budget is within the spec-decode
1420-
// batch size of the force-close threshold, hand off to AR for the
1421-
// tail. AR handles the close-token override cleanly; spec-decode's
1422-
// verify-and-accept loop can't safely inject a token mid-batch
1423-
// without a KV-state rewrite.
1424-
//
1425-
// IMPORTANT: cache_.last_tok is set during do_prefill (line 701)
1426-
// and NEVER updated by the spec-decode commit loop — local
1427-
// `last_tok` here is the authoritative most-recently-committed
1428-
// token. Sync it into cache_.last_tok before handing off so AR's
1429-
// `first_tok = cache_.last_tok` seed is correct. Without this
1430-
// sync, AR would re-seed from the prefill's last argmax (stale
1431-
// by `n_generated` positions) and produce garbage continuation.
1432-
if (budget_hook && !budget_hook->close_token_ids.empty()) {
1433-
int hard = budget_hook->hard_limit_remaining;
1434-
// Tail when remaining <= hard + one spec-decode batch worth of
1435-
// headroom. Ensures the force-close fires within the AR tail
1436-
// rather than after a final spec-decode batch overshoots.
1437-
if (need_commit_budget <= hard + q_len) {
1438-
std::fprintf(stderr,
1439-
"[budget-hook] spec-decode tail-off at committed=%d/%d "
1440-
"(remaining=%d, hard_limit=%d, batch=%d) — switching to AR\n",
1441-
committed, n_gen, need_commit_budget, hard, q_len);
1442-
step_graph_destroy(draft_sg);
1443-
cache_.last_tok = last_tok; // sync spec-decode → AR seed
1444-
// Build a fresh hook keyed off this call's local n_gen
1445-
// (the remaining decode budget) so force-close fires once
1446-
// remaining <= hard_limit relative to AR's loop counter,
1447-
// not relative to the global decode budget. Without this
1448-
// remap force-close would either fire on every iter
1449-
// (negative remaining_for_hook) or never (positive but
1450-
// misscaled).
1451-
BudgetHook tail_hook = *budget_hook;
1452-
int ar_n_gen = need_commit_budget;
1453-
bool ok = do_ar_decode(committed, ar_n_gen, out_tokens, io,
1454-
tail_hook, forced_close_out,
1455-
degenerate_close_out);
1456-
io.emit(-1);
1457-
return ok;
1458-
}
1459-
}
1419+
// Budget hook: no tail-off here. The close-token injection fires
1420+
// during the emit phase (step 8) after acceptance+replay, mirroring
1421+
// the existing floor_to_ar pattern. This keeps spec-decode active
1422+
// through the close boundary instead of tearing down to AR early.
14601423

14611424
if (last_tok < 0 && !out_tokens.empty()) {
14621425
std::fprintf(stderr,
@@ -1633,6 +1596,7 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
16331596
bool hit_eos = false;
16341597
bool floor_to_ar = false;
16351598
bool inject_tool_prefix = false;
1599+
bool budget_close_fired = false;
16361600
constexpr size_t kActionSuffixLookback = 16;
16371601
constexpr size_t kSkipSequenceLookback = 64;
16381602
int emitted = 0;
@@ -1668,10 +1632,38 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
16681632
break;
16691633
}
16701634
}
1635+
// Budget hook: check if remaining budget has hit the force-close
1636+
// threshold. Override this token with close_token_ids[0] and stop
1637+
// emitting — AR handles the rest via maybe_force_close.
1638+
if (budget_hook && !budget_hook->close_token_ids.empty() &&
1639+
!budget_close_fired)
1640+
{
1641+
const int generated_now = n_generated + emitted;
1642+
int remaining = n_gen - generated_now;
1643+
if (remaining <= budget_hook->hard_limit_remaining) {
1644+
int32_t first_close = budget_hook->close_token_ids.front();
1645+
if (replay_tok[i] == first_close) {
1646+
// Model self-closed at the boundary; consume as
1647+
// start of close sequence (no override needed).
1648+
budget_close_fired = true;
1649+
} else {
1650+
// Force-close: override sampled token with close[0].
1651+
replay_tok[i] = first_close;
1652+
budget_close_fired = true;
1653+
if (forced_close_out) *forced_close_out = true;
1654+
}
1655+
std::fprintf(stderr,
1656+
"[budget-hook] spec-decode close at committed=%d/%d "
1657+
"(remaining=%d <= hard_limit=%d)\n",
1658+
committed + emitted, n_gen, remaining,
1659+
budget_hook->hard_limit_remaining);
1660+
}
1661+
}
16711662
out_tokens.push_back(replay_tok[i]);
16721663
io.emit(replay_tok[i]);
16731664
emitted++;
16741665
if (io.cancelled) break;
1666+
if (budget_close_fired) break;
16751667
if (IS_EOS_TOK(replay_tok[i], w_)) { hit_eos = true; break; }
16761668
}
16771669
int injected = 0;
@@ -1747,6 +1739,47 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen,
17471739
io.emit(-1);
17481740
return ok;
17491741
}
1742+
// Budget hook close: close token was emitted during the emit phase.
1743+
// Roll back KV to pre-replay state and replay only the overridden
1744+
// prefix (including the close token) so KV stays consistent with
1745+
// the emitted output before AR takes over.
1746+
if (budget_close_fired) {
1747+
if (!target->restore_kv()) {
1748+
step_graph_destroy(draft_sg);
1749+
return false;
1750+
}
1751+
cache_.cur_pos = committed;
1752+
if (emitted > 0) {
1753+
std::vector<int32_t> replay_prefix(replay_tok.begin(),
1754+
replay_tok.begin() + emitted);
1755+
int prefix_last_tok = -1;
1756+
if (!target->verify_batch(replay_prefix, committed,
1757+
prefix_last_tok, nullptr)) {
1758+
std::fprintf(stderr, "spec-decode: budget-close prefix replay failed\n");
1759+
step_graph_destroy(draft_sg);
1760+
return false;
1761+
}
1762+
}
1763+
committed += emitted;
1764+
cache_.cur_pos = committed;
1765+
step_graph_destroy(draft_sg);
1766+
cache_.last_tok = out_tokens.empty() ? last_tok : out_tokens.back();
1767+
const int total_draft_pos = std::max(1, n_draft_steps * q_len);
1768+
out_accept_rate =
1769+
(float)((double)n_accept_sum / (double)total_draft_pos);
1770+
const int ar_n_gen = n_gen - n_generated;
1771+
if (ar_n_gen <= 0) {
1772+
io.emit(-1);
1773+
return true;
1774+
}
1775+
BudgetHook tail_hook = budget_hook ? *budget_hook : BudgetHook{};
1776+
tail_hook.close_token_ids.clear();
1777+
bool ok = do_ar_decode(committed, ar_n_gen, out_tokens, io,
1778+
tail_hook, forced_close_out,
1779+
degenerate_close_out);
1780+
io.emit(-1);
1781+
return ok;
1782+
}
17501783
if (hit_eos) break;
17511784
}
17521785

0 commit comments

Comments
 (0)