use super::*;
use crate::scheduler::io::{DecodeRows, Readback, StepOutcome};
mod content_emit;
#[cfg(test)]
mod eos_hold_tests;
mod host_sample;
mod per_token;
thread_local! {
static PAR_SAMPLE_SCRATCH: crate::scheduler::sched_ctx::DecodeScratch =
crate::scheduler::sched_ctx::DecodeScratch::default();
}
fn logits_ctx<'a>(
sched: &'a crate::scheduler::sched_ctx::SchedCtx,
scratch: &'a crate::scheduler::sched_ctx::DecodeScratch,
think_end_token: Option<u32>,
think_start_token: Option<u32>,
tool_call_start_token: Option<u32>,
tool_call_end_token: Option<u32>,
) -> crate::scheduler::logit_processors::LogitsContext<'a> {
crate::scheduler::logit_processors::LogitsContext {
think_end_token,
think_start_token,
tool_call_start_token,
tool_call_end_token,
thinking_closed_by: sched.limits.thinking_closed_by,
verify_pos: 0,
watchdog: sched.watchdog,
scratch,
tel: &*sched.io.tel,
clock: &*sched.io.clock,
boundary_mask: sched.masks.boundary.clone(),
mid_word_mask: sched.masks.mid_word.clone(),
sampling: sched.levers.sampling(),
}
}
fn decode_timing_record(
sched: &crate::scheduler::sched_ctx::SchedCtx,
copy_us: u64,
sample_us: u64,
) {
use std::sync::atomic::Ordering;
if !sched.levers.decode_timing {
return;
}
let stats = sched.io.tel.stats();
stats.decode_copy_us.fetch_add(copy_us, Ordering::Relaxed);
stats
.decode_sample_us
.fetch_add(sample_us, Ordering::Relaxed);
let n = stats.decode_count.fetch_add(1, Ordering::Relaxed) + 1;
if n.is_multiple_of(100) {
let c = stats.decode_copy_us.swap(0, Ordering::Relaxed);
let s = stats.decode_sample_us.swap(0, Ordering::Relaxed);
stats.decode_count.store(0, Ordering::Relaxed);
tracing::info!(
"DECODE_TIMING (last 100 host-path tokens): copy+fwd-wait={:.2}ms/tok sample(248k host)={:.2}ms/tok",
c as f64 / 100_000.0,
s as f64 / 100_000.0,
);
}
}
pub(super) fn decode_readback_plan<'a>(
active: &[ActiveSeq],
sched: &crate::scheduler::sched_ctx::SchedCtx,
staging: &'a mut Vec<u8>,
) -> Readback<'a> {
if argmax_readback_eligible(active.iter(), sched) {
Readback::Argmax
} else {
Readback::HostLogits { into: staging }
}
}
pub(super) fn argmax_readback_eligible<'a>(
rows: impl Iterator<Item = &'a ActiveSeq> + Clone,
sched: &crate::scheduler::sched_ctx::SchedCtx,
) -> bool {
let active = rows;
let any_grammar = active.clone().any(|a| a.grammar_state.is_some());
let any_logprobs = active.clone().any(|a| a.top_logprobs.is_some());
let model_logits_fp32 = sched.io.dev.model().decode_logits_fp32();
let think_ended_gpu_ok = |a: &ActiveSeq| {
a.think_ended
&& !a.inside_thinking
&& a.grammar_state.is_none()
&& a.repetition_penalty == 1.0
&& a.presence_penalty == 0.0
&& a.frequency_penalty == 0.0
&& a.lz_penalty == 0.0
&& a.dry_multiplier == 0.0
};
let admit_think_ended = sched.levers.think_ended_gpu_argmax;
let needs_host_logits = active.clone().any(|a| {
let excused = admit_think_ended && think_ended_gpu_ok(a);
(a.inside_thinking || a.think_ended || a.grammar_state.is_some()) && !excused
}) || any_logprobs
|| model_logits_fp32
|| active.clone().any(|a| {
crate::scheduler::eos_hold::presample_hold_for(a, a.output_tokens.len()).any()
});
active.clone().all(|a| a.temperature == 0.0) && !any_grammar && !needs_host_logits
}
pub(super) fn decode_readback_or_fail(
active: &mut Vec<ActiveSeq>,
logits: DevicePtr,
sched: &crate::scheduler::sched_ctx::SchedCtx,
staging: &mut Vec<u8>,
) -> Option<StepOutcome> {
let model = sched.io.dev.model();
let n = active.len();
let readback = decode_readback_plan(active, sched, staging);
match crate::scheduler::io::sync_device::execute_readback(model, logits, n, readback) {
Ok(rows) => Some(StepOutcome::Decode { logits, rows }),
Err(e) => {
tracing::error!("argmax_batch error: {e:#}");
for mut a in active.drain(..) {
send_error(&sched.io, &mut a, &format!("{e:#}"));
}
None
}
}
}
pub fn process_decode_logits(
active: &mut Vec<ActiveSeq>,
step: StepOutcome,
staging: &mut Vec<u8>,
t0: std::time::Instant,
think_end_token: Option<u32>,
think_start_token: Option<u32>,
code_fence_token: Option<u32>,
tool_call_start_token: Option<u32>,
tool_call_end_token: Option<u32>,
adaptive_sampling: bool,
sched: &crate::scheduler::sched_ctx::SchedCtx,
) {
process_decode_logits_skipping(
active,
step,
staging,
t0,
think_end_token,
think_start_token,
code_fence_token,
tool_call_start_token,
tool_call_end_token,
adaptive_sampling,
sched,
&[],
)
}
pub fn process_decode_logits_skipping(
active: &mut Vec<ActiveSeq>,
step: StepOutcome,
staging: &mut Vec<u8>,
t0: std::time::Instant,
think_end_token: Option<u32>,
think_start_token: Option<u32>,
code_fence_token: Option<u32>,
tool_call_start_token: Option<u32>,
tool_call_end_token: Option<u32>,
adaptive_sampling: bool,
sched: &crate::scheduler::sched_ctx::SchedCtx,
skip: &[bool],
) {
let skipped = |i: usize| skip.get(i).copied().unwrap_or(false);
let n = active.len();
let model = sched.io.dev.model();
let StepOutcome::Decode { logits, rows } = step;
let host_elem = match &rows {
DecodeRows::HostLogits { elem_bytes } => Some(*elem_bytes),
DecodeRows::Tokens(_) => None,
};
let fast_tokens: Option<Vec<(u32, Option<crate::api::TokenLogprobs>)>> = match rows {
DecodeRows::Tokens(t) => {
let hit_mask = t.iter().zip(active.iter()).any(|(&tok, a)| {
a.think_ended && (Some(tok) == think_end_token || Some(tok) == a.think_start_token)
});
if hit_mask {
sched
.think_mask_fallbacks
.set(sched.think_mask_fallbacks.get() + 1);
None
} else {
Some(t.into_iter().map(|tok| (tok, None)).collect())
}
}
DecodeRows::HostLogits { .. } => None,
};
let new_tokens: Vec<(u32, Option<crate::api::TokenLogprobs>)> = if let Some(t) = fast_tokens {
t
} else {
match host_sample::sample_on_host(
active,
staging,
logits,
host_elem,
n,
model,
think_end_token,
think_start_token,
tool_call_start_token,
tool_call_end_token,
adaptive_sampling,
sched,
&skipped,
) {
Some(sampled) => sampled,
None => return,
}
};
*sched.scratch.host_bytes.borrow_mut() = std::mem::take(staging);
let step_ms = sched
.io
.clock
.now()
.saturating_duration_since(t0)
.as_secs_f64()
* 1000.0;
if tracing::enabled!(tracing::Level::DEBUG) {
let token_ids: Vec<u32> = new_tokens.iter().map(|(t, _)| *t).collect();
tracing::debug!(
"DECODE: n={n} step={step_ms:.1}ms ({:.1} tok/s) tokens={:?}",
1000.0 * n as f64 / step_ms,
token_ids,
);
}
let now = sched.io.clock.now();
for (i, (tok, logprobs)) in new_tokens.into_iter().enumerate() {
if skipped(i) {
continue;
}
let a = &mut active[i];
per_token::process_decoded_token(
a,
tok,
logprobs,
now,
think_end_token,
think_start_token,
code_fence_token,
tool_call_start_token,
tool_call_end_token,
model,
sched,
);
}
}