use crate::logits::{StopSequence, TokenId};
use onnx_genai_kv::{CachePriority, DEFAULT_CHUNK_SIZE, KvDType, LocalTieredConfig, SequenceId};
use onnx_genai_ort::{Eagle3DraftKvMode, MtpDraftKvMode};
use onnx_genai_scheduler::{Priority, SchedulerConfig};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MtpConfig {
pub head_model: PathBuf,
pub target_hidden_output: String,
pub embedding_weights: PathBuf,
pub lm_head_weights: PathBuf,
pub vocab_size: usize,
pub hidden_size: usize,
pub kv_mode: MtpDraftKvMode,
pub num_speculative_tokens: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Eagle3Config {
pub head_model: PathBuf,
pub target_hidden_outputs: Vec<String>,
pub embedding_weights: PathBuf,
pub vocab_size: usize,
pub hidden_size: usize,
pub kv_mode: Eagle3DraftKvMode,
pub num_speculative_tokens: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedKvProposerConfig {
pub assistant_model: PathBuf,
pub target_hidden_output: String,
pub input_embedding_weights: PathBuf,
pub backbone_hidden_size: usize,
pub vocab_size: usize,
pub num_speculative_tokens: usize,
pub shared_kv: Vec<SharedKvBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedKvBinding {
pub name: String,
pub target_layers: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SpeculativeMode {
#[default]
None,
DraftModel,
PromptLookup {
ngram: usize,
max_tokens: usize,
},
Mtp(MtpConfig),
Eagle3(Eagle3Config),
SharedKv(SharedKvProposerConfig),
}
pub type SessionId = SequenceId;
#[derive(Debug, Clone, Default)]
pub enum KvConnectorBackend {
#[default]
Null,
LocalTiered(LocalTieredConfig),
}
#[derive(Debug, Clone)]
pub struct KvConnectorConfig {
pub backend: KvConnectorBackend,
pub model_id: Option<String>,
pub chunk_size: usize,
pub store_priority: CachePriority,
pub recompute_ms_per_token: f64,
}
impl Default for KvConnectorConfig {
fn default() -> Self {
Self {
backend: KvConnectorBackend::Null,
model_id: None,
chunk_size: DEFAULT_CHUNK_SIZE,
store_priority: CachePriority::Session,
recompute_ms_per_token: 0.05,
}
}
}
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub num_gpu_pages: usize,
pub page_size: usize,
pub scheduler: SchedulerConfig,
pub draft_model: Option<PathBuf>,
pub num_speculative_tokens: usize,
pub speculative_mode: SpeculativeMode,
pub kv_cache_dtype: KvDType,
pub kv_connector: KvConnectorConfig,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
num_gpu_pages: 1024,
page_size: 16,
scheduler: SchedulerConfig::default(),
draft_model: None,
num_speculative_tokens: 4,
speculative_mode: SpeculativeMode::None,
kv_cache_dtype: KvDType::F32,
kv_connector: KvConnectorConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneratePrompt {
Text(String),
TokenIds(Vec<TokenId>),
}
impl From<String> for GeneratePrompt {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<&str> for GeneratePrompt {
fn from(value: &str) -> Self {
Self::Text(value.to_string())
}
}
impl From<Vec<TokenId>> for GeneratePrompt {
fn from(value: Vec<TokenId>) -> Self {
Self::TokenIds(value)
}
}
#[derive(Debug, Clone)]
pub struct GenerateOptions {
pub max_new_tokens: usize,
pub temperature: f32,
pub top_p: f32,
pub top_k: usize,
pub min_p: f32,
pub repetition_penalty: f32,
pub frequency_penalty: f32,
pub presence_penalty: f32,
pub greedy: bool,
pub seed: Option<u64>,
pub stop_sequences: Vec<StopSequence>,
pub eos_token_id: Option<TokenId>,
pub stop_on_eos: bool,
pub max_context: Option<usize>,
pub num_speculative_tokens: Option<usize>,
pub speculative_mode: Option<SpeculativeMode>,
pub constraint: Option<GenerateConstraint>,
pub top_logprobs: Option<usize>,
}
impl Default for GenerateOptions {
fn default() -> Self {
Self {
max_new_tokens: 128,
temperature: 1.0,
top_p: 1.0,
top_k: 0,
min_p: 0.0,
repetition_penalty: 1.0,
frequency_penalty: 0.0,
presence_penalty: 0.0,
greedy: true,
seed: None,
stop_sequences: Vec::new(),
eos_token_id: None,
stop_on_eos: true,
max_context: None,
num_speculative_tokens: None,
speculative_mode: None,
constraint: None,
top_logprobs: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GenerateConstraint {
Json,
JsonSchema(String),
Regex(String),
Lark(String),
}
impl GenerateOptions {
pub(crate) fn validate(&self) -> anyhow::Result<()> {
if self.max_new_tokens == 0 {
anyhow::bail!("max_new_tokens must be greater than zero");
}
if !self.temperature.is_finite() || self.temperature < 0.0 {
anyhow::bail!("temperature must be finite and non-negative");
}
if !self.top_p.is_finite() || self.top_p < 0.0 {
anyhow::bail!("top_p must be finite and non-negative");
}
if !self.min_p.is_finite() || !(0.0..=1.0).contains(&self.min_p) {
anyhow::bail!("min_p must be finite and between 0 and 1");
}
if !self.repetition_penalty.is_finite() || self.repetition_penalty <= 0.0 {
anyhow::bail!("repetition_penalty must be finite and greater than zero");
}
if !self.frequency_penalty.is_finite() {
anyhow::bail!("frequency_penalty must be finite");
}
if !self.presence_penalty.is_finite() {
anyhow::bail!("presence_penalty must be finite");
}
if self.max_context == Some(0) {
anyhow::bail!("max_context must be greater than zero when provided");
}
if self.num_speculative_tokens == Some(0) {
anyhow::bail!("num_speculative_tokens must be greater than zero when provided");
}
if let Some(SpeculativeMode::PromptLookup { ngram, max_tokens }) = &self.speculative_mode {
if *ngram == 0 {
anyhow::bail!("prompt-lookup ngram must be greater than zero");
}
if *max_tokens == 0 {
anyhow::bail!("prompt-lookup max_tokens must be greater than zero");
}
}
if let Some(SpeculativeMode::Mtp(config)) = &self.speculative_mode {
validate_mtp_config(config)?;
}
if let Some(SpeculativeMode::Eagle3(config)) = &self.speculative_mode {
validate_eagle3_config(config)?;
}
if let Some(SpeculativeMode::SharedKv(config)) = &self.speculative_mode {
validate_shared_kv_proposer_config(config)?;
}
Ok(())
}
}
pub(crate) fn validate_mtp_config(config: &MtpConfig) -> anyhow::Result<()> {
if config.target_hidden_output.is_empty() {
anyhow::bail!("MTP target_hidden_output must not be empty");
}
if config.vocab_size == 0 || config.hidden_size == 0 {
anyhow::bail!("MTP vocab_size and hidden_size must be greater than zero");
}
if config.num_speculative_tokens == 0 {
anyhow::bail!("MTP num_speculative_tokens must be greater than zero");
}
Ok(())
}
pub(crate) fn validate_eagle3_config(config: &Eagle3Config) -> anyhow::Result<()> {
if config.target_hidden_outputs.len() != 3
|| config
.target_hidden_outputs
.iter()
.any(|name| name.is_empty())
{
anyhow::bail!(
"EAGLE-3 target_hidden_outputs must contain exactly three non-empty low/middle/high output names"
);
}
if config.vocab_size == 0 || config.hidden_size == 0 {
anyhow::bail!("EAGLE-3 vocab_size and hidden_size must be greater than zero");
}
if config.num_speculative_tokens == 0 {
anyhow::bail!("EAGLE-3 num_speculative_tokens must be greater than zero");
}
Ok(())
}
pub(crate) fn validate_shared_kv_proposer_config(
config: &SharedKvProposerConfig,
) -> anyhow::Result<()> {
if config.target_hidden_output.is_empty() {
anyhow::bail!("shared-KV proposer target_hidden_output must not be empty");
}
if config.backbone_hidden_size == 0 || config.vocab_size == 0 {
anyhow::bail!("shared-KV proposer backbone_hidden_size and vocab_size must be greater than zero");
}
if config.num_speculative_tokens == 0 {
anyhow::bail!("shared-KV proposer num_speculative_tokens must be greater than zero");
}
if config.shared_kv.is_empty() {
anyhow::bail!("shared-KV proposer requires at least one shared_kv binding group");
}
for group in &config.shared_kv {
if group.name.is_empty() {
anyhow::bail!("shared-KV proposer shared_kv group name must not be empty");
}
if group.target_layers.is_empty() {
anyhow::bail!(
"shared-KV proposer shared_kv group '{}' must list at least one target layer",
group.name
);
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct GenerateRequest {
pub prompt: GeneratePrompt,
pub options: GenerateOptions,
}
impl GenerateRequest {
pub fn new(prompt: impl Into<GeneratePrompt>) -> Self {
Self {
prompt: prompt.into(),
options: GenerateOptions::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct PrioritizedGenerateRequest {
pub session_id: SessionId,
pub request: GenerateRequest,
pub priority: Priority,
}
#[derive(Debug, Clone)]
pub struct ScheduledGenerateArrival {
pub arrival_step: usize,
pub request: PrioritizedGenerateRequest,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PrioritizedGenerateResult {
pub session_id: SessionId,
pub result: GenerateResult,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FinishReason {
MaxTokens,
EosToken,
StopSequence { index: usize },
Length,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GenerateResult {
pub text: String,
pub token_ids: Vec<TokenId>,
pub finish_reason: FinishReason,
pub prefix_cache_hit_len: usize,
pub logprobs: Option<Vec<TokenLogprob>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TokenLogprob {
pub token_id: TokenId,
pub logprob: f32,
pub top: Vec<(TokenId, f32)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerateToken {
pub token_id: TokenId,
pub text: String,
pub finish_reason: Option<FinishReason>,
}
pub type GenerateTokenCallback<'a> = dyn FnMut(GenerateToken) -> anyhow::Result<()> + Send + 'a;