Skip to content

Commit 61f3bb4

Browse files
committed
server: restore multi-slot DFlash adaptive batching
Add a shared cohort draft cap for flat DFlash adaptive scheduling so cold/off slots do not break multi-slot verifier batching. Record multi-slot profit baseline samples for cold/off slots and include post-loop MTP draft time in cycle profiling.
1 parent 63abcd3 commit 61f3bb4

4 files changed

Lines changed: 192 additions & 42 deletions

File tree

tests/test-adaptive-dm.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,27 @@ int main() {
10991099
assert(sweep.profit_next_off_probe_depth(15, 3) == expected_order[0]);
11001100
}
11011101

1102+
// Multi-slot flat DFlash verification needs equal row counts. If any
1103+
// eligible slot already has a positive adaptive depth, cold/off slots join a
1104+
// shared cohort depth instead of forcing a mixed speculative/baseline batch.
1105+
{
1106+
const int one_hot_normal[] = {15, 0};
1107+
const int one_hot_cap[] = {15, 15};
1108+
assert(server_adaptive_dm_resolve_cohort_n_max(one_hot_normal, one_hot_cap, 2) == 15);
1109+
1110+
const int mixed_positive_normal[] = {15, 8};
1111+
const int mixed_positive_cap[] = {15, 8};
1112+
assert(server_adaptive_dm_resolve_cohort_n_max(mixed_positive_normal, mixed_positive_cap, 2) == 8);
1113+
1114+
const int all_cold_normal[] = {0, 0};
1115+
const int all_cold_cap[] = {15, 15};
1116+
assert(server_adaptive_dm_resolve_cohort_n_max(all_cold_normal, all_cold_cap, 2) == 0);
1117+
1118+
const int blocked_normal[] = {15, 0};
1119+
const int blocked_cap[] = {15, 0};
1120+
assert(server_adaptive_dm_resolve_cohort_n_max(blocked_normal, blocked_cap, 2) == 0);
1121+
}
1122+
11021123
// Failed wake probes advance the off-state sweep instead of retrying the
11031124
// same shallow depth forever.
11041125
{

tests/test-dflash-plumbing.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,19 @@ int main(int argc, char ** argv) {
12081208
server_context.find("common_speculative_get_draft_params(slot.get_spec(), slot.id)") != std::string::npos &&
12091209
server_context.find("/* .n_past = */ draft_n_past") != std::string::npos,
12101210
"server must seed upstream MTP drafting through the shared upstream dparams path using slot.prompt.n_tokens()");
1211+
{
1212+
const size_t mtp_draft_phase = server_context.find("// Phase 2: single draft call for all collected MTP slots");
1213+
const size_t mtp_timer = server_context.find("const int64_t t_mtp_draft_start = ggml_time_us();", mtp_draft_phase);
1214+
const size_t mtp_draft = server_context.find("common_speculative_draft(spec.get());", mtp_timer);
1215+
const size_t mtp_account = server_context.find("t_draft_total += ggml_time_us() - t_mtp_draft_start;", mtp_draft);
1216+
ok &= expect(mtp_draft_phase != std::string::npos &&
1217+
mtp_timer != std::string::npos &&
1218+
mtp_draft != std::string::npos &&
1219+
mtp_account != std::string::npos &&
1220+
mtp_timer < mtp_draft &&
1221+
mtp_draft < mtp_account,
1222+
"server must account post-loop MTP draft time as draft_ms instead of hiding it in other_ms");
1223+
}
12111224
ok &= expect(server_context.find("common_speculative_accept(slot.get_spec(), slot.id, n_accepted_draft)") != std::string::npos,
12121225
"server must accept upstream MTP drafts through the per-sequence shared speculative accept path");
12131226
ok &= expect(server_context.find("slot.spec_ckpt.update_pos(") != std::string::npos,

tools/server/server-adaptive-dm.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,41 @@ static inline int server_adaptive_dm_build_candidates(int base_n_max, int * out,
100100
return n;
101101
}
102102

103+
static inline int server_adaptive_dm_resolve_cohort_n_max(
104+
const int * normal_n_max,
105+
const int * cohort_cap_n_max,
106+
int n_slots) {
107+
if (!normal_n_max || !cohort_cap_n_max || n_slots < 2) {
108+
return 0;
109+
}
110+
111+
int n_eligible = 0;
112+
int n_positive = 0;
113+
int cohort_n_max = INT_MAX;
114+
for (int i = 0; i < n_slots; ++i) {
115+
const int slot_cap = cohort_cap_n_max[i];
116+
if (slot_cap <= 0) {
117+
continue;
118+
}
119+
120+
n_eligible++;
121+
if (normal_n_max[i] > 0) {
122+
n_positive++;
123+
}
124+
125+
const int slot_limit = normal_n_max[i] > 0
126+
? std::min(normal_n_max[i], slot_cap)
127+
: slot_cap;
128+
cohort_n_max = std::min(cohort_n_max, slot_limit);
129+
}
130+
131+
if (n_eligible < 2 || n_positive == 0 || cohort_n_max == INT_MAX) {
132+
return 0;
133+
}
134+
135+
return std::max(0, cohort_n_max);
136+
}
137+
103138
static inline int server_adaptive_dm_next_explore_depth(int current_n, int base_n_max, float probe_fraction) {
104139
if (base_n_max <= 0) {
105140
return 0;

tools/server/server-context.cpp

Lines changed: 123 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,10 @@ struct server_slot : server_adaptive_dm_state {
10091009
generated_token_probs.push_back(token);
10101010
}
10111011

1012-
int get_n_draft_max(const common_params & global_params, bool advance_adaptive_probe = false) {
1012+
int get_n_draft_max(
1013+
const common_params & global_params,
1014+
bool advance_adaptive_probe = false,
1015+
int dflash_cohort_n_max = -1) {
10131016
GGML_ASSERT(task);
10141017

10151018
if (!can_speculate()) {
@@ -1033,43 +1036,58 @@ struct server_slot : server_adaptive_dm_state {
10331036
: configured_base_n_max;
10341037
const int probe_n_max = server_adaptive_dm_probe_n_max(base_n_max, dm_probe_fraction);
10351038
int n_draft_max = (dm_adaptive && adaptive_n_max >= 0) ? std::min((int) adaptive_n_max, base_n_max) : base_n_max;
1039+
const bool use_dflash_cohort_n_max =
1040+
global_params.speculative.type() == COMMON_SPECULATIVE_TYPE_DFLASH &&
1041+
global_params.speculative.branch_budget == 0 &&
1042+
dflash_cohort_n_max > 0;
10361043

10371044
if (use_profit_controller) {
1038-
if (profit_baseline_probe_pending) {
1045+
if (profit_baseline_probe_pending && use_dflash_cohort_n_max) {
1046+
n_draft_max = dflash_cohort_n_max;
1047+
} else if (profit_baseline_probe_pending) {
10391048
return 0;
1040-
}
1041-
1042-
if (profit_should_probe_baseline()) {
1049+
} else if (profit_should_probe_baseline() && use_dflash_cohort_n_max) {
1050+
n_draft_max = dflash_cohort_n_max;
1051+
} else if (profit_should_probe_baseline()) {
10431052
if (advance_adaptive_probe) {
10441053
profit_mark_baseline_probe();
10451054
}
10461055
return 0;
1047-
}
1048-
1049-
if (adaptive_n_max < 0) {
1056+
} else if (adaptive_n_max < 0) {
10501057
if (advance_adaptive_probe) {
10511058
adaptive_n_max = 0;
10521059
}
1053-
return 0;
1054-
} else if (adaptive_n_max == 0) {
1055-
if (!profit_baseline_ready()) {
1060+
if (!use_dflash_cohort_n_max) {
10561061
return 0;
10571062
}
1058-
const bool probe_now = adaptive_probe_counter + 1 >= profit_off_probe_interval();
1059-
if (!probe_now) {
1060-
if (advance_adaptive_probe) {
1061-
adaptive_probe_counter++;
1063+
n_draft_max = dflash_cohort_n_max;
1064+
} else if (adaptive_n_max == 0) {
1065+
if (!profit_baseline_ready()) {
1066+
if (!use_dflash_cohort_n_max) {
1067+
return 0;
1068+
}
1069+
n_draft_max = dflash_cohort_n_max;
1070+
} else {
1071+
const bool probe_now = adaptive_probe_counter + 1 >= profit_off_probe_interval();
1072+
if (!probe_now) {
1073+
if (advance_adaptive_probe) {
1074+
adaptive_probe_counter++;
1075+
}
1076+
if (!use_dflash_cohort_n_max) {
1077+
return 0;
1078+
}
1079+
n_draft_max = dflash_cohort_n_max;
1080+
} else {
1081+
const int profit_probe_n_max = [&]() {
1082+
return profit_next_off_probe_depth(base_n_max, probe_n_max);
1083+
}();
1084+
if (advance_adaptive_probe) {
1085+
adaptive_probe_counter = 0;
1086+
adaptive_n_max = profit_probe_n_max;
1087+
}
1088+
n_draft_max = profit_probe_n_max;
10621089
}
1063-
return 0;
1064-
}
1065-
const int profit_probe_n_max = [&]() {
1066-
return profit_next_off_probe_depth(base_n_max, probe_n_max);
1067-
}();
1068-
if (advance_adaptive_probe) {
1069-
adaptive_probe_counter = 0;
1070-
adaptive_n_max = profit_probe_n_max;
10711090
}
1072-
n_draft_max = profit_probe_n_max;
10731091
} else {
10741092
n_draft_max = std::min((int) adaptive_n_max, base_n_max);
10751093
if (advance_adaptive_probe) {
@@ -1115,6 +1133,10 @@ struct server_slot : server_adaptive_dm_state {
11151133
}
11161134
}
11171135

1136+
if (use_dflash_cohort_n_max && n_draft_max > 0) {
1137+
n_draft_max = std::min(n_draft_max, dflash_cohort_n_max);
1138+
}
1139+
11181140
// determine the max draft that fits the current slot state
11191141
// note: slot.prompt is not yet expanded with the `id` token sampled above
11201142
// also, need to leave space for 1 extra token to allow context shifts
@@ -4384,8 +4406,8 @@ struct server_context_impl {
43844406
}
43854407
};
43864408
int n_slots_drafted = 0;
4387-
server_slot * profit_baseline_slot = nullptr;
43884409
int n_profit_baseline_slots = 0;
4410+
std::vector<server_slot *> profit_baseline_slots;
43894411
server_slot * dflash_cycle_slot = nullptr;
43904412
int dflash_cycle_n_draft = 0;
43914413
int dflash_cycle_n_accept = 0;
@@ -4405,12 +4427,53 @@ struct server_context_impl {
44054427
std::any_of(slots.begin(), slots.end(), [](const server_slot & slot) {
44064428
return slot.state == SLOT_STATE_STARTED || slot.state == SLOT_STATE_PROCESSING_PROMPT;
44074429
});
4430+
std::vector<int> dflash_cohort_n_draft_max(slots.size(), -1);
4431+
if (params_base.speculative.type() == COMMON_SPECULATIVE_TYPE_DFLASH &&
4432+
params_base.speculative.branch_budget == 0 &&
4433+
!dflash_recurrent_has_pending_prompt) {
4434+
std::vector<int> normal_n_max(slots.size(), 0);
4435+
std::vector<int> cohort_cap_n_max(slots.size(), 0);
4436+
4437+
for (auto & slot : slots) {
4438+
if (slot.state != SLOT_STATE_GENERATING ||
4439+
!slot.can_speculate() ||
4440+
slot.id < 0 ||
4441+
slot.id >= (int) slots.size()) {
4442+
continue;
4443+
}
4444+
4445+
normal_n_max[slot.id] = slot.get_n_draft_max(params_base, false);
4446+
cohort_cap_n_max[slot.id] = normal_n_max[slot.id] > 0
4447+
? normal_n_max[slot.id]
4448+
: slot.get_n_draft_max(params_base, false, INT_MAX);
4449+
}
4450+
4451+
const int cohort_n_max = server_adaptive_dm_resolve_cohort_n_max(
4452+
normal_n_max.data(), cohort_cap_n_max.data(), (int) slots.size());
4453+
if (cohort_n_max > 0) {
4454+
for (auto & slot : slots) {
4455+
if (slot.id < 0 || slot.id >= (int) slots.size() ||
4456+
cohort_cap_n_max[slot.id] <= 0) {
4457+
continue;
4458+
}
4459+
dflash_cohort_n_draft_max[slot.id] = std::min(cohort_n_max, cohort_cap_n_max[slot.id]);
4460+
}
4461+
}
4462+
}
4463+
auto dflash_scheduler_n_draft_max = [&](server_slot & slot, bool advance_adaptive_probe) {
4464+
if (slot.id >= 0 && slot.id < (int) dflash_cohort_n_draft_max.size() &&
4465+
dflash_cohort_n_draft_max[slot.id] > 0) {
4466+
return slot.get_n_draft_max(
4467+
params_base, advance_adaptive_probe, dflash_cohort_n_draft_max[slot.id]);
4468+
}
4469+
return slot.get_n_draft_max(params_base, advance_adaptive_probe);
4470+
};
44084471
if (ctx_dft_shared) {
44094472
int n_drafting = 0;
44104473
for (auto & slot : slots) {
44114474
if (slot.state == SLOT_STATE_GENERATING &&
44124475
slot.can_speculate() &&
4413-
slot.get_n_draft_max(params_base, false) > 0 &&
4476+
dflash_scheduler_n_draft_max(slot, false) > 0 &&
44144477
!dflash_recurrent_has_pending_prompt) {
44154478
n_drafting++;
44164479
}
@@ -4427,7 +4490,7 @@ struct server_context_impl {
44274490
for (auto & slot : slots) {
44284491
if (slot.state == SLOT_STATE_GENERATING &&
44294492
slot.can_speculate() &&
4430-
slot.get_n_draft_max(params_base, false) > 0 &&
4493+
dflash_scheduler_n_draft_max(slot, false) > 0 &&
44314494
!dflash_recurrent_has_pending_prompt) {
44324495
batch_specs.push_back(slot.get_spec());
44334496
batch_id_lasts.push_back(slot.sampled);
@@ -4437,10 +4500,21 @@ struct server_context_impl {
44374500

44384501
std::vector<llama_tokens> batch_results;
44394502
std::vector<std::vector<float>> batch_log_probs;
4503+
common_params_speculative params_batch = params_base.speculative;
4504+
int batch_cohort_n_max = INT_MAX;
4505+
for (const int slot_id : batch_slot_ids) {
4506+
if (slot_id >= 0 && slot_id < (int) dflash_cohort_n_draft_max.size() &&
4507+
dflash_cohort_n_draft_max[slot_id] > 0) {
4508+
batch_cohort_n_max = std::min(batch_cohort_n_max, dflash_cohort_n_draft_max[slot_id]);
4509+
}
4510+
}
4511+
if (batch_cohort_n_max != INT_MAX) {
4512+
params_batch.n_max = std::min(params_batch.n_max, batch_cohort_n_max);
4513+
}
44404514
const int64_t t_batch_start = ggml_time_us();
44414515
common_speculative_draft_batch(
44424516
batch_specs, ctx_dft_shared.get(),
4443-
params_base.speculative, batch_id_lasts, batch_results,
4517+
params_batch, batch_id_lasts, batch_results,
44444518
use_rejection_sampling ? &batch_log_probs : nullptr);
44454519
t_draft_total = ggml_time_us() - t_batch_start;
44464520
llama_set_dflash_n_slots(ctx_dft_shared.get(), 1);
@@ -4475,7 +4549,7 @@ struct server_context_impl {
44754549
continue;
44764550
}
44774551

4478-
int n_draft_max = slot.get_n_draft_max(params_base, true);
4552+
int n_draft_max = dflash_scheduler_n_draft_max(slot, true);
44794553
if (params_base.speculative.type() == COMMON_SPECULATIVE_TYPE_DFLASH &&
44804554
params_base.speculative.branch_budget == 0) {
44814555
n_draft_max = dflash_flat_effective_draft_max(ctx_dft_shared.get(), n_draft_max);
@@ -4757,8 +4831,8 @@ struct server_context_impl {
47574831
if (slot.can_speculate() && slot.dm_adaptive &&
47584832
server_adaptive_dm_uses_profit_controller(slot.dm_controller) &&
47594833
slot.profit_expects_baseline_sample()) {
4760-
profit_baseline_slot = &slot;
47614834
n_profit_baseline_slots++;
4835+
profit_baseline_slots.push_back(&slot);
47624836
}
47634837

47644838
slot.i_batch = batch.n_tokens;
@@ -4775,7 +4849,9 @@ struct server_context_impl {
47754849
// --- MTP post-loop phases (upstream-aligned) ---
47764850
// Phase 2: single draft call for all collected MTP slots
47774851
if (!mtp_drafting.empty()) {
4852+
const int64_t t_mtp_draft_start = ggml_time_us();
47784853
common_speculative_draft(spec.get());
4854+
t_draft_total += ggml_time_us() - t_mtp_draft_start;
47794855
}
47804856

47814857
// Phase 3: checkpoint creation and batch building for MTP slots
@@ -7042,19 +7118,24 @@ struct server_context_impl {
70427118
}
70437119
llama_set_dflash_consume_reduced(ctx_tgt, false);
70447120

7045-
if (pure_tg_cycle && n_slots_drafted == 0 && n_profit_baseline_slots == 1 && n_tg_tokens == 1 &&
7046-
profit_baseline_slot && profit_baseline_slot->task &&
7047-
profit_baseline_slot->state == SLOT_STATE_GENERATING) {
7121+
if (pure_tg_cycle && n_slots_drafted == 0 && n_profit_baseline_slots > 0) {
70487122
const int64_t t_cycle_total_now = ggml_time_us() - t_cycle_start;
7049-
profit_baseline_slot->observe_profit_timing(
7050-
0,
7051-
0,
7052-
0,
7053-
t_draft_total / 1000.0f,
7054-
t_verify_total / 1000.0f,
7055-
t_accept_total / 1000.0f,
7056-
t_cycle_total_now / 1000.0f);
7057-
apply_profit_decision(*profit_baseline_slot);
7123+
const float baseline_shared_scale = n_tg_tokens > 1 ? 1.0f / (float) n_tg_tokens : 1.0f;
7124+
for (server_slot * slot_ptr : profit_baseline_slots) {
7125+
if (!slot_ptr || !slot_ptr->task || slot_ptr->state != SLOT_STATE_GENERATING) {
7126+
continue;
7127+
}
7128+
7129+
slot_ptr->observe_profit_timing(
7130+
0,
7131+
0,
7132+
0,
7133+
(t_draft_total / 1000.0f) * baseline_shared_scale,
7134+
(t_verify_total / 1000.0f) * baseline_shared_scale,
7135+
(t_accept_total / 1000.0f) * baseline_shared_scale,
7136+
(t_cycle_total_now / 1000.0f) * baseline_shared_scale);
7137+
apply_profit_decision(*slot_ptr);
7138+
}
70587139
}
70597140

70607141
// --- profiling: log per-cycle breakdown ---

0 commit comments

Comments
 (0)