#[cfg(test)]
pub(super) use super::decode_launch::choose_decode_victim;
use super::decode_launch::fail_readback;
pub(super) use super::decode_launch::{
FeedReadback, PREEMPT_IMMUNITY_TOKENS, decode_ctx_commit, launch_with_preemption,
};
use super::*;
use crate::scheduler::io::{Effect, SchedIo, SpillIo, StepOutcome};
pub(super) fn decode_batch_with_preemption(
sched: &crate::scheduler::sched_ctx::SchedCtx,
active: &mut Vec<ActiveSeq>,
spill: Option<&dyn SpillIo>,
swapped: &mut Vec<SwappedSeq>,
preempted: &mut Vec<PreemptedSeq>,
staging: &mut Vec<u8>,
) -> Option<StepOutcome> {
let ticket = launch_with_preemption(
sched,
active,
spill,
swapped,
preempted,
staging,
FeedReadback::Plain,
)?;
match sched.io.dev.await_result(ticket) {
Ok(r) => Some(r.outcome),
Err(e) => {
fail_readback(&sched.io, active, &e.into_inner());
None
}
}
}
#[allow(clippy::result_large_err)]
pub(super) fn spill_out_sequence(
io: &SchedIo,
mut a: ActiveSeq,
spill: &dyn SpillIo,
) -> Result<SwappedSeq, (ActiveSeq, anyhow::Error)> {
let (swap_id, mut writer) = match spill.create() {
Ok(v) => v,
Err(e) => return Err((a, e)),
};
if let Err(e) = io.dev.apply(Effect::SaveSequenceState {
seq: &a.seq,
writer: &mut writer,
}) {
drop(writer);
let _ = spill.remove(swap_id);
return Err((a, e.into_inner()));
}
drop(writer);
spill.record_usage(swap_id);
let num_blocks = a.seq.block_table.len();
let seq_len = a.seq.seq_len;
let tokens = a.seq.tokens.clone();
let _ = io.dev.apply(Effect::ReleaseSeq {
seq: &mut a.seq,
cache: false,
what: "spill_out_sequence",
});
Ok(SwappedSeq {
tokens,
session_hash: a.session_hash,
lookup_tenant: a.seq.lookup_tenant,
adapter_slot: a.seq.adapter_slot,
adapter_id: a.seq.adapter_id,
seq_len,
num_blocks,
last_token: a.last_token,
output_tokens: a.output_tokens,
remaining: a.remaining,
min_tokens: a.min_tokens,
eos_tokens: a.eos_tokens,
sink: a.sink,
temperature: a.temperature,
top_k: a.top_k,
top_p: a.top_p,
top_n_sigma: a.top_n_sigma,
min_p: a.min_p,
repetition_penalty: a.repetition_penalty,
presence_penalty: a.presence_penalty,
frequency_penalty: a.frequency_penalty,
repetition_penalty_window: 256,
lz_penalty: DEFAULT_LZ_PENALTY,
dry_multiplier: a.dry_multiplier,
dry_base: a.dry_base,
dry_allowed_length: a.dry_allowed_length,
dry_sequence_breakers: a.dry_sequence_breakers,
logit_bias: a.logit_bias,
inside_thinking: a.inside_thinking,
enable_thinking: a.enable_thinking,
thinking_budget: a.thinking_budget,
repetition_detection: a.repetition_detection,
spontaneous_think_budget: a.spontaneous_think_budget,
thinking_tokens: a.thinking_tokens,
force_end_thinking: a.force_end_thinking,
think_force_closed: a.think_force_closed,
sentence_defer_count: a.sentence_defer_count,
consecutive_confident: a.consecutive_confident,
in_code_fence: a.in_code_fence,
think_end_token: a.think_end_token,
think_start_token: a.think_start_token,
think_ended: a.think_ended,
think_just_ended: a.think_just_ended,
post_think_emitted: a.post_think_emitted,
think_skip_count: a.think_skip_count,
require_tool_call: a.require_tool_call,
tool_request: a.tool_request,
tools_present: a.tools_present,
suppress_tool_call: a.suppress_tool_call,
disable_mtp: a.disable_mtp,
mtp_acct: a.mtp_acct,
content_started: a.content_started,
content_tokens: a.content_tokens,
prose_tokens_since_last_tool: a.prose_tokens_since_last_tool,
think_watchdog_fires: a.think_watchdog_fires,
rollback_count: a.rollback_count,
tool_call_start_token: a.tool_call_start_token,
tool_call_opened: a.tool_call_opened,
tool_call_end_token: a.tool_call_end_token,
last_token_time: a.last_token_time,
request_start: a.request_start,
decode_start: a.decode_start,
seed: a.seed,
top_logprobs: a.top_logprobs,
logprobs_data: a.logprobs_data,
timeout_at: a.timeout_at,
swap_id,
cached_prompt_tokens: a.cached_prompt_tokens,
})
}
pub(super) fn preempt_requeue(io: &SchedIo, mut a: ActiveSeq) -> PreemptedSeq {
let tokens = a.seq.tokens.clone();
let _ = io.dev.apply(Effect::ReleaseSeq {
seq: &mut a.seq,
cache: true,
what: "preempt_requeue",
});
a.pending_drafts.clear();
a.pending_draft_conf.clear();
a.spec_adapt = Default::default();
PreemptedSeq { a, tokens }
}
pub(super) fn resume_preempted_seq(
model: &dyn Model,
io: &SchedIo,
p: PreemptedSeq,
) -> Result<ActiveSeq> {
let PreemptedSeq { mut a, tokens } = p;
let mut seq = match model.alloc_sequence() {
Ok(s) => s,
Err(e) => {
send_error_to_sink(
io,
&mut a.sink,
&format!("preempt-resume alloc failed: {e:#}"),
);
return Err(e);
}
};
seq.session_hash = a.session_hash;
seq.lookup_tenant = a.seq.lookup_tenant;
seq.adapter_slot = a.seq.adapter_slot;
seq.adapter_id = a.seq.adapter_id;
seq.acquired_adapter_slot = model.acquire_adapter_slot(a.seq.adapter_slot);
let prefill_result = (|| -> Result<()> {
model.ep_broadcast_cmd_for_seq(seq.slot_idx as u32, 0xFFFFFFF0)?;
model.ep_broadcast_cmd(tokens.len() as u32)?;
model.ep_broadcast_cmd(0)?;
model.ep_broadcast_cmd(tokens.len() as u32)?;
model.ep_broadcast_tokens(&tokens)?;
model.ep_sync_vision_embeds(&tokens)?;
model.prefill(&tokens, &mut seq, 0)?;
Ok(())
})();
if let Err(e) = prefill_result {
a.seq = seq;
send_error(
io,
&mut a,
&format!("preempt-resume re-prefill failed: {e:#}"),
);
return Err(e);
}
a.seq = seq;
a.ssm_rollback_ring = SsmDecodeRing::new(model.decode_rollback_ring_slots());
a.preempt_immune_until_tokens = a.output_tokens.len() + PREEMPT_IMMUNITY_TOKENS;
Ok(a)
}
pub(super) fn resume_preempted_seqs(
model: &dyn Model,
io: &SchedIo,
active: &mut Vec<ActiveSeq>,
preempted: &mut Vec<PreemptedSeq>,
max_batch_size: usize,
block_size: usize,
) {
while !preempted.is_empty() && active.len() < max_batch_size {
let Some((idx, needed)) = preempted
.iter()
.enumerate()
.map(|(i, p)| (i, p.tokens.len() / block_size.max(1) + 1))
.min_by_key(|&(_, n)| n)
else {
return;
};
let want = needed + 1;
let total = model.num_total_blocks();
if total > 0 && want > total {
let mut p = preempted.remove(idx);
send_error_to_sink(
io,
&mut p.a.sink,
&format!(
"preempted sequence needs {want} KV blocks but the pool has {total}; \
cannot resume"
),
);
continue;
}
let p = &preempted[idx];
let reclaim = model.reclaim_prefix_blocks_for(&p.tokens, p.a.seq.adapter_slot, want);
if reclaim.free < reclaim.target {
return;
}
let p = preempted.remove(idx);
let n_tokens = p.tokens.len();
match resume_preempted_seq(model, io, p) {
Ok(a) => {
tracing::info!(
"Preempt-resume: re-prefilled {n_tokens} tokens \
({} generated so far), decode continues",
a.output_tokens.len(),
);
active.push(a);
}
Err(e) => tracing::error!("Preempt-resume failed: {e:#}"),
}
}
}