mod end_tick;
mod finish;
mod lane_decode;
mod lanes;
pub(super) mod pipeline;
mod pipeline_lane;
mod telemetry;
mod tick;
pub use pipeline::PipelineFaults;
use super::*;
use metrale_cache::kv_spill::KvSpillManager;
use metrale_scheduler::{Core, LaneVerdict, TickPlan};
pub enum Lane {
StartPrefills(Vec<InferenceRequest>),
ContinuePrefills,
Decode,
}
pub struct SchedulerCore {
ctx: crate::scheduler::sched_ctx::SchedCtx,
eos_tokens: Vec<u32>,
max_batch_size: usize,
dflash_verify_raw_argmax: bool,
num_drafts: usize,
policy: Box<dyn crate::scheduling_policy::SchedulingPolicy>,
max_prefill_tokens: usize,
max_batch_tokens: usize,
use_self_speculative: bool,
use_ngram_speculative: bool,
block_size: usize,
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>,
grammar_engine: Option<GrammarEngine>,
adaptive_sampling: bool,
session_manager: crate::session_manager::SessionSsmManager,
spontaneous_think_budget: Option<u32>,
use_mtp: bool,
chunked: bool,
mtp_gate: Option<metrale_speculative::mtp_gate::MtpGate>,
ngram_proposer: Option<NgramProposer>,
spec_slot_cap: usize,
always_mixed: bool,
admit_watermark: usize,
pending: PendingQueue,
prefill_stream: u64,
prefill_event: u64,
active: Vec<ActiveSeq>,
prefilling: Vec<PrefillInProgress>,
swapped: Vec<SwappedSeq>,
preempted: Vec<PreemptedSeq>,
snapshot_steps: u64,
did_mixed_step: bool,
pipeline: pipeline::Pipeline,
}
impl SchedulerCore {
pub fn new(
dev: Box<io::DynDevice>,
request_rx: tokio::sync::mpsc::Receiver<InferenceRequest>,
rotation_rx: tokio::sync::mpsc::Receiver<LoraRotation>,
cfg: config::SchedulerConfig,
) -> Self {
let config::SchedulerConfig {
eos_tokens,
max_batch_size,
use_speculative,
dflash_verify_raw_argmax,
num_drafts,
policy,
max_prefill_tokens,
max_batch_tokens,
use_self_speculative,
use_ngram_speculative,
prompt_lookup,
shared_lookup,
swap_space_gb,
high_speed_swap_cfg,
block_size,
think_end_token,
think_start_token,
code_fence_token,
tool_call_start_token,
tool_call_end_token,
grammar_engine,
adaptive_sampling,
session_manager,
spontaneous_think_budget,
vocab_masks,
limits,
watchdog,
levers,
snapshot,
dflash_rung,
telemetry,
pipeline_faults,
} = cfg;
let spill: Option<Box<dyn io::SpillIo>> = if swap_space_gb > 0 {
let max_bytes = swap_space_gb as u64 * 1024 * 1024 * 1024;
let spill_dir =
std::env::temp_dir().join(format!("metrale-swap-{}", std::process::id()));
match KvSpillManager::new(spill_dir.clone(), max_bytes) {
Ok(mgr) => {
tracing::info!("Swap space: {swap_space_gb} GB at {}", spill_dir.display());
Some(Box::new(io::FileSpill::new(mgr)))
}
Err(e) => {
tracing::error!("Failed to initialize swap space: {e:#}");
None
}
}
} else {
None
};
let mut sched = crate::scheduler::sched_ctx::SchedCtx::new(
vocab_masks,
levers,
io::SchedIo::serving(
dev,
telemetry,
snapshot,
io::TokioRequestIo::new(request_rx, rotation_rx),
spill,
),
limits,
watchdog,
metrale_speculative::adaptive_rung::AdaptiveRung::from_env(),
dflash_rung,
);
sched
.io
.dev
.model()
.bind_gpu_to_thread()
.expect("Failed to bind CUDA context to scheduler thread");
let use_mtp = use_speculative && sched.io.dev.model().has_proposer();
sched.prompt_lookup = prompt_lookup.filter(|_| use_mtp);
if let Some(pl) = sched.prompt_lookup {
tracing::info!(
"prompt-lookup decoding: ARMED (ngram={}, max_drafts={}, max_seqs={})",
pl.ngram,
pl.max_drafts,
pl.max_seqs
);
if let Some(setup) = shared_lookup {
let sl = crate::scheduler::shared_lookup_step::SharedLookup::new(setup)
.expect("shared prompt-lookup cache settings are validated at serve start");
tracing::info!(
"prompt-lookup shared cache: ARMED (budget={} MiB, scopes={}, key_len={})",
setup.config.budget_bytes >> 20,
setup.config.max_scopes,
setup.config.key_len
);
sched.shared_lookup = Some(sl);
}
}
let num_drafts = if use_mtp || use_self_speculative || use_ngram_speculative {
num_drafts.max(1)
} else {
0
};
let chunked = max_prefill_tokens > 0;
let mtp_gate = if use_mtp && !sched.levers.mtp_gate_force {
Some(metrale_speculative::mtp_gate::MtpGate::new(num_drafts))
} else {
if use_mtp && sched.levers.mtp_gate_force {
tracing::warn!(
"--mtp-gate force: MTP throughput gate DISARMED (diagnostic; \
verify runs even where the gate would measure it net-negative)"
);
}
None
};
let ngram_proposer = if use_ngram_speculative {
Some(NgramProposer::new(4))
} else {
None
};
tracing::info!(
"Scheduler started (batched mode, max_batch={max_batch_size}, mtp={}, ngram={}, num_drafts={num_drafts}, policy={}, chunked_prefill={}, max_prefill_tokens={})",
use_mtp,
use_ngram_speculative,
policy.name(),
chunked,
if chunked { max_prefill_tokens } else { 0 },
);
let spec_slot_cap = if sched.io.dev.model().has_ssm_layers() {
metrale_model_layers::ssm_reserve::mtp_state_slots(max_batch_size)
} else {
max_batch_size
};
if spec_slot_cap < max_batch_size {
tracing::info!(
"MTP verify pools cover {spec_slot_cap}/{max_batch_size} SSM slots — \
sequences on uncovered slots plain-decode until compaction moves them \
down (kill switch METRALE_MTP_POOL_FULL_WIDTH restores full width)"
);
}
let always_mixed = sched.levers.holo_always_mixed;
if always_mixed {
tracing::info!(
"METRALE_HOLO_ALWAYS_MIXED=on: fused mixed step always-on (slice-budget)"
);
}
let admit_watermark = admission::resolve_admit_watermark(sched.limits.max_seq_len);
let pending = PendingQueue::new();
let prefill_stream = sched
.io
.dev
.model()
.create_stream()
.expect("Failed to create prefill CUDA stream");
let prefill_event = sched
.io
.dev
.model()
.create_event()
.expect("Failed to create prefill CUDA event");
let active: Vec<ActiveSeq> = Vec::new();
let prefilling: Vec<PrefillInProgress> = Vec::new();
let swapped: Vec<SwappedSeq> = Vec::new();
let preempted: Vec<PreemptedSeq> = Vec::new();
install_high_speed_swap(sched.io.dev.model(), high_speed_swap_cfg);
Self {
ctx: sched,
eos_tokens,
max_batch_size,
dflash_verify_raw_argmax,
num_drafts,
policy,
max_prefill_tokens,
max_batch_tokens,
use_self_speculative,
use_ngram_speculative,
block_size,
think_end_token,
think_start_token,
code_fence_token,
tool_call_start_token,
tool_call_end_token,
grammar_engine,
adaptive_sampling,
session_manager,
spontaneous_think_budget,
use_mtp,
chunked,
mtp_gate,
ngram_proposer,
spec_slot_cap,
always_mixed,
admit_watermark,
pending,
prefill_stream,
prefill_event,
active,
prefilling,
swapped,
preempted,
snapshot_steps: 0,
did_mixed_step: false,
pipeline: pipeline::Pipeline::new(pipeline_faults),
}
}
fn commit_tokens(&self) -> pipeline::CommitTokens {
pipeline::CommitTokens {
think_end_token: self.think_end_token,
think_start_token: self.think_start_token,
code_fence_token: self.code_fence_token,
tool_call_start_token: self.tool_call_start_token,
tool_call_end_token: self.tool_call_end_token,
adaptive_sampling: self.adaptive_sampling,
}
}
fn drain_pipeline(&mut self) {
let toks = self.commit_tokens();
pipeline::drain(&mut self.active, &mut self.pipeline, &toks, &self.ctx);
}
}
impl Core for SchedulerCore {
type Lane = Lane;
fn plan(&mut self) -> TickPlan<Lane> {
self.ctx.io.tel.step_begin();
self.plan_tick()
}
fn execute_lane(&mut self, lane: Lane) -> LaneVerdict {
self.execute_lane_measured(lane)
}
fn end_tick(&mut self) {
self.end_tick_phases()
}
fn finish(self) {
self.finish_run()
}
}