use crate::decode::{
DecodeState, clone_value, extract_next_token_logits, run_decode_step_with_extra,
};
use crate::decode_loop::{DecodeLoopBackend, DecodeLoopState, run_decode_loop};
use crate::engine::{Engine, EngineConfig};
use crate::kv_bridge::infer_kv_model_info;
use crate::logits::TokenId;
use crate::processors::build_processor_chain;
use crate::{GeneratePrompt, GenerateRequest, GenerateResult, GenerateTokenCallback};
use anyhow::Context;
use onnx_genai_metadata::{
DataflowEdge, PhaseRunOn, PipelineSpec, PipelineStrategy, PipelineStrategyKind,
};
use onnx_genai_ort::{PipelineModels, Session, SessionOptions, Tokenizer, Value};
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
pub type PipelineTensors = HashMap<String, Value>;
pub struct PipelineGenerateRequest {
pub request: GenerateRequest,
pub inputs: PipelineTensors,
}
impl PipelineGenerateRequest {
pub fn new(request: GenerateRequest) -> Self {
Self {
request,
inputs: HashMap::new(),
}
}
pub fn with_input(mut self, endpoint: impl Into<String>, value: Value) -> Self {
self.inputs.insert(endpoint.into(), value);
self
}
}
impl From<GenerateRequest> for PipelineGenerateRequest {
fn from(request: GenerateRequest) -> Self {
Self::new(request)
}
}
pub struct PipelineEngine {
models: PipelineModels,
plan: PipelinePlan,
decoder_state: DecodeState,
tokenizer_component: String,
}
impl Engine {
pub fn from_pipeline_dir(
pipeline_dir: &Path,
config: EngineConfig,
) -> anyhow::Result<PipelineEngine> {
PipelineEngine::from_dir_with_config(pipeline_dir, config)
}
}
impl PipelineEngine {
pub fn from_dir(pipeline_dir: &Path) -> anyhow::Result<Self> {
Self::from_dir_with_config(pipeline_dir, EngineConfig::default())
}
pub fn from_dir_with_config(pipeline_dir: &Path, config: EngineConfig) -> anyhow::Result<Self> {
let models = PipelineModels::load_with_options(pipeline_dir, SessionOptions::default())
.map_err(|e| anyhow::anyhow!("Failed to load pipeline models: {}", e))?;
let plan = PipelinePlan::from_spec(&models.directory.spec)?;
let decoder = models
.session(&plan.decoder)
.with_context(|| format!("pipeline decoder '{}' was not loaded", plan.decoder))?;
let _kv_model = infer_kv_model_info(decoder, config.page_size)?;
let decoder_state = DecodeState::new(decoder)?;
let tokenizer_component = plan.decoder.clone();
Ok(Self {
models,
plan,
decoder_state,
tokenizer_component,
})
}
pub fn generate(&mut self, request: GenerateRequest) -> anyhow::Result<GenerateResult> {
self.generate_with_pipeline_request(request.into())
}
pub fn generate_with_pipeline_request(
&mut self,
pipeline_request: PipelineGenerateRequest,
) -> anyhow::Result<GenerateResult> {
self.generate_with_callback(pipeline_request, None)
}
pub fn generate_with_callback(
&mut self,
pipeline_request: PipelineGenerateRequest,
mut callback: Option<&mut GenerateTokenCallback<'_>>,
) -> anyhow::Result<GenerateResult> {
let mut options = pipeline_request.request.options.clone();
options.validate()?;
if options.eos_token_id.is_none() {
options.eos_token_id = self.tokenizer()?.eos_token_id();
}
let prompt_tokens = tokenize_with(self.tokenizer()?, &pipeline_request.request.prompt)?;
if prompt_tokens.is_empty() {
anyhow::bail!("prompt must contain at least one token");
}
let mut tensors = pipeline_request.inputs;
self.run_prompt_phase_components(&mut tensors)?;
let decoder_extras = self.decoder_extra_inputs(&tensors)?;
let chain = build_processor_chain(&options, Some(self.tokenizer()?))?;
self.decoder_state = {
let decoder = self.models.session(&self.plan.decoder).with_context(|| {
format!("pipeline decoder '{}' was not loaded", self.plan.decoder)
})?;
DecodeState::new(decoder)?
};
let decoder = self
.models
.session(&self.plan.decoder)
.with_context(|| format!("pipeline decoder '{}' was not loaded", self.plan.decoder))?;
let tokenizer = self
.models
.tokenizer_for(&self.tokenizer_component)
.with_context(|| {
format!("no tokenizer available for '{}'", self.tokenizer_component)
})?;
let mut backend = PipelineDecodeLoopBackend {
decoder,
decoder_state: &mut self.decoder_state,
decoder_extras: &decoder_extras,
context_tokens: prompt_tokens,
prompt_len: 0,
generated_count: 0,
};
backend.prompt_len = backend.context_tokens.len();
let mut loop_state = DecodeLoopState::new(0);
run_decode_loop(
&mut backend,
&mut loop_state,
&options,
&chain,
tokenizer,
None,
callback.as_deref_mut(),
)
}
pub fn spec(&self) -> &PipelineSpec {
&self.models.directory.spec
}
fn tokenizer(&self) -> anyhow::Result<&Tokenizer> {
self.models
.tokenizer_for(&self.tokenizer_component)
.with_context(|| format!("no tokenizer available for '{}'", self.tokenizer_component))
}
fn run_prompt_phase_components(&self, tensors: &mut PipelineTensors) -> anyhow::Result<()> {
for component in &self.plan.prompt_components {
let session = self
.models
.session(component)
.with_context(|| format!("pipeline component '{component}' was not loaded"))?;
let inputs = self.component_inputs(component, session, tensors)?;
let refs = inputs
.iter()
.map(|(name, value)| (name.as_str(), value))
.collect::<Vec<_>>();
let outputs = session
.run(&refs)
.map_err(|e| anyhow::anyhow!("ORT pipeline component '{component}' failed: {e}"))?;
for (name, value) in session.output_names().iter().zip(outputs.into_iter()) {
tensors.insert(format!("{component}.{name}"), value);
}
}
Ok(())
}
fn component_inputs(
&self,
component: &str,
session: &Session,
tensors: &PipelineTensors,
) -> anyhow::Result<Vec<(String, Value)>> {
let mut inputs = Vec::new();
for info in session.inputs() {
let endpoint = format!("{component}.{}", info.name);
let routed = self
.plan
.dataflow
.iter()
.find(|edge| edge.to == endpoint)
.and_then(|edge| tensors.get(&edge.from));
let value = tensors
.get(&endpoint)
.or(routed)
.with_context(|| format!("missing pipeline input '{endpoint}'"))?;
inputs.push((info.name.clone(), clone_value(value)?));
}
Ok(inputs)
}
fn decoder_extra_inputs(
&self,
tensors: &PipelineTensors,
) -> anyhow::Result<Vec<(String, Value)>> {
let mut extras = Vec::new();
for edge in self
.plan
.edges_to_component(&self.plan.decoder)
.filter(|edge| {
endpoint_component(&edge.from).is_some_and(|from| from != self.plan.decoder)
})
{
let (_, input) = parse_endpoint(&edge.to)?;
let value = tensors
.get(&edge.from)
.with_context(|| format!("missing routed pipeline tensor '{}'", edge.from))?;
extras.push((input.to_string(), clone_value(value)?));
}
Ok(extras)
}
}
fn tokenize_with(tokenizer: &Tokenizer, prompt: &GeneratePrompt) -> anyhow::Result<Vec<TokenId>> {
match prompt {
GeneratePrompt::TokenIds(tokens) => Ok(tokens.clone()),
GeneratePrompt::Text(text) => tokenizer
.encode(text)
.map_err(|e| anyhow::anyhow!("Failed to tokenize prompt: {}", e)),
}
}
struct PipelineDecodeLoopBackend<'a> {
decoder: &'a Session,
decoder_state: &'a mut DecodeState,
decoder_extras: &'a [(String, Value)],
context_tokens: Vec<TokenId>,
prompt_len: usize,
generated_count: usize,
}
impl DecodeLoopBackend for PipelineDecodeLoopBackend<'_> {
fn context_len(&self) -> usize {
self.context_tokens.len()
}
fn processor_prompt_tokens(&self) -> Vec<TokenId> {
self.context_tokens.clone()
}
fn next_logits(&mut self) -> anyhow::Result<Vec<f32>> {
let past_len = if self.decoder_state.use_kv {
self.context_tokens
.len()
.saturating_sub(if self.generated_count == 0 {
self.prompt_len
} else {
1
})
} else {
0
};
let input_tokens = if self.decoder_state.use_kv && self.generated_count > 0 {
self.context_tokens[self.context_tokens.len() - 1..].to_vec()
} else {
self.context_tokens.clone()
};
let outputs = run_decode_step_with_extra(
self.decoder,
self.decoder_state,
&input_tokens,
past_len,
self.decoder_extras,
)?;
extract_next_token_logits(self.decoder, outputs)
}
fn commit_token(&mut self, token_id: TokenId) -> anyhow::Result<()> {
self.context_tokens.push(token_id);
self.generated_count += 1;
Ok(())
}
}
#[derive(Debug, Clone)]
struct PipelinePlan {
decoder: String,
prompt_components: Vec<String>,
dataflow: Vec<DataflowEdge>,
}
impl PipelinePlan {
fn from_spec(spec: &PipelineSpec) -> anyhow::Result<Self> {
let decoder = autoregressive_decoder(&spec.strategy)
.context("pipeline strategy must contain an autoregressive decoder")?;
if !spec.models.contains_key(&decoder) {
anyhow::bail!("pipeline decoder '{decoder}' is not declared in models");
}
let mut prompt_components = Vec::new();
for component in topological_components(spec)? {
if component == decoder {
continue;
}
match component_phase(spec, &component, &decoder) {
PhaseRunOn::PromptOnly => prompt_components.push(component),
PhaseRunOn::EveryStep | PhaseRunOn::OnDemand | PhaseRunOn::FinalOnly => {}
PhaseRunOn::Other(value) => {
anyhow::bail!(
"unsupported phase '{value}' for pipeline component '{component}'"
)
}
}
}
Ok(Self {
decoder,
prompt_components,
dataflow: spec.dataflow.clone(),
})
}
fn edges_to_component<'a>(
&'a self,
component: &'a str,
) -> impl Iterator<Item = &'a DataflowEdge> + 'a {
self.dataflow
.iter()
.filter(move |edge| endpoint_component(&edge.to) == Some(component))
}
}
fn autoregressive_decoder(strategy: &PipelineStrategy) -> Option<String> {
match strategy.kind {
PipelineStrategyKind::Autoregressive => strategy.decoder.clone(),
PipelineStrategyKind::Composite => strategy
.stages
.iter()
.find_map(|stage| autoregressive_decoder(&stage.strategy)),
PipelineStrategyKind::Iterative
| PipelineStrategyKind::SinglePass
| PipelineStrategyKind::Other(_) => None,
}
}
fn component_phase(spec: &PipelineSpec, component: &str, decoder: &str) -> PhaseRunOn {
spec.phases
.get(component)
.map(|phase| phase.run_on.clone())
.unwrap_or_else(|| {
if component == decoder {
PhaseRunOn::EveryStep
} else {
PhaseRunOn::PromptOnly
}
})
}
fn topological_components(spec: &PipelineSpec) -> anyhow::Result<Vec<String>> {
let mut remaining = spec.models.keys().cloned().collect::<BTreeSet<_>>();
let mut ordered = Vec::new();
while !remaining.is_empty() {
let ready = remaining
.iter()
.find(|component| {
spec.dataflow.iter().all(|edge| {
endpoint_component(&edge.to) != Some(component.as_str())
|| endpoint_component(&edge.from)
.is_some_and(|from| !remaining.contains(from))
})
})
.cloned();
let Some(component) = ready else {
anyhow::bail!("pipeline dataflow contains a cycle");
};
remaining.remove(&component);
ordered.push(component);
}
Ok(ordered)
}
fn parse_endpoint(endpoint: &str) -> anyhow::Result<(&str, &str)> {
endpoint
.split_once('.')
.filter(|(component, port)| !component.is_empty() && !port.is_empty())
.with_context(|| format!("pipeline endpoint must be component.port: {endpoint}"))
}
fn endpoint_component(endpoint: &str) -> Option<&str> {
parse_endpoint(endpoint)
.ok()
.map(|(component, _)| component)
}
#[cfg(test)]
mod tests {
use super::*;
use onnx_genai_metadata::{PhaseConfig, PipelineComponentSpec, PipelineStrategyStage};
use std::collections::BTreeMap;
fn component(role: &str) -> PipelineComponentSpec {
PipelineComponentSpec {
filename: format!("{role}.onnx"),
role: role.to_string(),
device_preference: None,
tokenizer: None,
}
}
#[test]
fn plan_routes_prompt_encoder_outputs_to_decoder_inputs() -> anyhow::Result<()> {
let spec = PipelineSpec {
models: BTreeMap::from([
("vision_encoder".to_string(), component("encoder")),
("decoder".to_string(), component("decoder")),
]),
dataflow: vec![DataflowEdge {
from: "vision_encoder.image_features".to_string(),
to: "decoder.encoder_hidden_states".to_string(),
dtype: Some("fp32".to_string()),
device_transfer: Some(false),
}],
strategy: PipelineStrategy {
kind: PipelineStrategyKind::Composite,
decoder: None,
max_tokens: None,
stop_conditions: None,
kv_cache: None,
speculative: None,
model: None,
batching: None,
denoiser: None,
scheduler: None,
num_steps: None,
guidance_scale: None,
state: None,
stages: vec![
PipelineStrategyStage {
name: "encode".to_string(),
strategy: Box::new(PipelineStrategy {
kind: PipelineStrategyKind::SinglePass,
decoder: None,
max_tokens: None,
stop_conditions: None,
kv_cache: None,
speculative: None,
model: Some("vision_encoder".to_string()),
batching: None,
denoiser: None,
scheduler: None,
num_steps: None,
guidance_scale: None,
state: None,
stages: vec![],
}),
run_on: Some(PhaseRunOn::PromptOnly),
},
PipelineStrategyStage {
name: "decode".to_string(),
strategy: Box::new(PipelineStrategy {
kind: PipelineStrategyKind::Autoregressive,
decoder: Some("decoder".to_string()),
max_tokens: None,
stop_conditions: None,
kv_cache: None,
speculative: None,
model: None,
batching: None,
denoiser: None,
scheduler: None,
num_steps: None,
guidance_scale: None,
state: None,
stages: vec![],
}),
run_on: Some(PhaseRunOn::EveryStep),
},
],
},
phases: BTreeMap::from([
(
"vision_encoder".to_string(),
PhaseConfig {
run_on: PhaseRunOn::PromptOnly,
},
),
(
"decoder".to_string(),
PhaseConfig {
run_on: PhaseRunOn::EveryStep,
},
),
]),
};
let plan = PipelinePlan::from_spec(&spec)?;
assert_eq!(plan.prompt_components, ["vision_encoder"]);
assert_eq!(plan.decoder, "decoder");
let routed = plan.edges_to_component("decoder").collect::<Vec<_>>();
assert_eq!(routed.len(), 1);
assert_eq!(
parse_endpoint(&routed[0].to)?,
("decoder", "encoder_hidden_states")
);
assert_eq!(routed[0].from, "vision_encoder.image_features");
Ok(())
}
}