use std::error::Error;
use std::fmt;
use std::ops::Range;
use rten::{Dimension, NodeId, RunOptions, Value, ValueOrView, ValueView};
use rten_tensor::prelude::*;
use rten_tensor::{NdTensor, Tensor};
#[cfg(feature = "text-decoder")]
use rten_text::{Tokenizer, TokenizerError};
use crate::filter::LogitsFilter;
use crate::metrics::Metrics;
use crate::model::Model;
use crate::sampler::{ArgMaxSampler, Sampler};
#[cfg(feature = "text-decoder")]
use crate::text_decoder::TextDecoder;
pub type TokenId = u32;
#[derive(Debug)]
pub enum GeneratorError {
InputNotFound(String),
OutputNotFound(String),
ShapeMismatch(String),
GenerateError(Box<dyn Error>),
#[cfg(feature = "text-decoder")]
DecodeError(TokenizerError),
}
impl fmt::Display for GeneratorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GeneratorError::InputNotFound(name) => write!(f, "model input not found: {}", name),
GeneratorError::OutputNotFound(name) => write!(f, "model output not found: {}", name),
GeneratorError::ShapeMismatch(err) => write!(f, "shape mismatch: {}", err),
GeneratorError::GenerateError(err) => write!(f, "generation error: {}", err),
#[cfg(feature = "text-decoder")]
GeneratorError::DecodeError(err) => write!(f, "decode error: {}", err),
}
}
}
impl Error for GeneratorError {}
#[derive(Debug)]
struct ErrorContext {
error: Box<dyn Error>,
context: String,
}
impl Error for ErrorContext {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.error.as_ref())
}
}
impl std::fmt::Display for ErrorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.context, self.error)
}
}
enum KvCacheData {
BatchSeqChans(NdTensor<f32, 3>),
BatchHeadSeqChans(NdTensor<f32, 4>),
}
impl KvCacheData {
fn with_capacity(
batch_size: usize,
n_heads: Option<usize>,
size: usize,
seq_len_capacity: usize,
) -> KvCacheData {
if let Some(n_heads) = n_heads {
KvCacheData::BatchHeadSeqChans(NdTensor::with_capacity(
[batch_size, n_heads, seq_len_capacity, size],
2,
))
} else {
KvCacheData::BatchSeqChans(NdTensor::with_capacity(
[batch_size, seq_len_capacity, size],
1,
))
}
}
fn sequence_len(&self) -> usize {
match self {
KvCacheData::BatchSeqChans(data) => data.size(1),
KvCacheData::BatchHeadSeqChans(data) => data.size(2),
}
}
fn has_capacity(&self, sequence_len: usize) -> bool {
match self {
KvCacheData::BatchSeqChans(data) => {
data.has_capacity(1 , sequence_len)
}
KvCacheData::BatchHeadSeqChans(data) => {
data.has_capacity(2 , sequence_len)
}
}
}
fn clone_with_capacity(&self, max_sequence_len: usize) -> KvCacheData {
let max_sequence_len = max_sequence_len.max(self.sequence_len());
match self {
KvCacheData::BatchSeqChans(data) => {
let [batch, _seq, chans] = data.shape();
let mut new_data =
NdTensor::with_capacity([batch, max_sequence_len, chans], 1 );
new_data.append(1, data).expect("should have capacity");
KvCacheData::BatchSeqChans(new_data)
}
KvCacheData::BatchHeadSeqChans(data) => {
let [batch, n_heads, _seq, chans] = data.shape();
let mut new_data = NdTensor::with_capacity(
[batch, n_heads, max_sequence_len, chans],
2,
);
new_data.append(2, data).expect("should have capacity");
KvCacheData::BatchHeadSeqChans(new_data)
}
}
}
}
struct KvCache {
input_id: NodeId,
output_id: NodeId,
cache: Option<KvCacheData>,
}
pub struct KVCachePattern<'a> {
pub prefix: &'a str,
pub suffix: &'a str,
}
impl<'a> From<(&'a str, &'a str)> for KVCachePattern<'a> {
fn from(value: (&'a str, &'a str)) -> Self {
let (prefix, suffix) = value;
KVCachePattern { prefix, suffix }
}
}
pub struct KVCachePair<'a> {
pub input: KVCachePattern<'a>,
pub output: KVCachePattern<'a>,
pub encoder: bool,
}
pub struct ModelInputsConfig<'a> {
pub input_ids: &'a str,
pub logits: &'a str,
pub attention_mask: &'a str,
pub cache_position: &'a str,
pub kv_caches: Vec<KVCachePair<'a>>,
pub position_ids: &'a str,
pub use_cache_flag: &'a str,
}
#[derive(Default)]
pub struct GeneratorConfig<'a> {
pub model_inputs: ModelInputsConfig<'a>,
pub kv_cache_capacity: Option<usize>,
}
impl Default for ModelInputsConfig<'_> {
fn default() -> Self {
ModelInputsConfig {
input_ids: "input_ids",
logits: "logits",
attention_mask: "attention_mask",
cache_position: "cache_position",
position_ids: "position_ids",
use_cache_flag: "use_cache_branch",
kv_caches: [
KVCachePair {
input: ("past_key_values.", ".decoder.key").into(),
output: ("present.", ".decoder.key").into(),
encoder: false,
},
KVCachePair {
input: ("past_key_values.", ".decoder.value").into(),
output: ("present.", ".decoder.value").into(),
encoder: false,
},
KVCachePair {
input: ("past_key_values.", ".encoder.key").into(),
output: ("present.", ".encoder.key").into(),
encoder: true,
},
KVCachePair {
input: ("past_key_values.", ".encoder.value").into(),
output: ("present.", ".encoder.value").into(),
encoder: true,
},
KVCachePair {
input: ("past_key_values.", ".key").into(),
output: ("present.", ".key").into(),
encoder: false,
},
KVCachePair {
input: ("past_key_values.", ".value").into(),
output: ("present.", ".value").into(),
encoder: false,
},
]
.into(),
}
}
}
pub struct Generator<'a> {
model: &'a dyn Model,
run_options: Option<RunOptions>,
constant_inputs: Vec<(NodeId, ValueOrView<'a>)>,
constant_prop_inputs: Option<Vec<(NodeId, Value)>>,
#[allow(clippy::type_complexity)]
varying_inputs: Vec<(NodeId, &'a dyn Fn(usize, Range<usize>) -> ValueOrView<'a>)>,
input_ids: Vec<TokenId>,
input_offset: usize,
input_ids_input: NodeId,
logits_output: NodeId,
logits_filter: Option<Box<dyn LogitsFilter + 'a>>,
sampler: Box<dyn Sampler + 'a>,
prev_tokens: Vec<u32>,
kv_cache: Vec<KvCache>,
encoder_kv_cache: Vec<KvCache>,
}
impl<'a> Generator<'a> {
pub fn from_model(model: &'a dyn Model) -> Result<Generator<'a>, GeneratorError> {
let config = GeneratorConfig {
model_inputs: ModelInputsConfig::default(),
kv_cache_capacity: None,
};
Self::from_model_config(model, config)
}
pub fn from_model_config(
model: &'a dyn Model,
config: GeneratorConfig,
) -> Result<Generator<'a>, GeneratorError> {
let model_inputs = &config.model_inputs;
let input_ids_input =
model
.find_node(model_inputs.input_ids)
.ok_or(GeneratorError::InputNotFound(
model_inputs.input_ids.to_string(),
))?;
let logits_output =
model
.find_node(model_inputs.logits)
.ok_or(GeneratorError::OutputNotFound(
model_inputs.logits.to_string(),
))?;
let batch_size = 1;
let mut kv_cache = Vec::new();
let mut encoder_kv_cache = Vec::new();
for &input_id in model.input_ids() {
let input_info = model
.node_info(input_id)
.ok_or(GeneratorError::InputNotFound(format!(
"input ID {}",
input_id
)))?;
let name = input_info.name();
let Some(kv_pattern) = model_inputs
.kv_caches
.iter()
.find(|pat| name.starts_with(pat.input.prefix) && name.ends_with(pat.input.suffix))
else {
continue;
};
let (n_heads, size) = match *input_info.shape() {
[_, Dimension::Fixed(n_heads), _, Dimension::Fixed(size)] => (Some(n_heads), size),
[_, _, Dimension::Fixed(size)] => (None, size),
_ => {
return Err(GeneratorError::ShapeMismatch(format!(
"input \"{}\" has unexpected shape. expected (batch, past_seq_len, chans) or (batch, heads, past_seq_len, chans) where `heads` and `size` are fixed",
name
)));
}
};
let prefix = kv_pattern.input.prefix;
let layer_index_start = prefix.len();
let layer_index_end = name.len() - kv_pattern.input.suffix.len();
let layer_index_str = &name[layer_index_start..layer_index_end];
let Ok(layer_index) = layer_index_str.parse::<u32>() else {
continue;
};
let output_prefix = kv_pattern.output.prefix;
let output_suffix = kv_pattern.output.suffix;
let output_name = format!("{}{}{}", output_prefix, layer_index, output_suffix);
let output_id = model
.find_node(&output_name)
.ok_or(GeneratorError::OutputNotFound(output_name))?;
let max_seq_len = config.kv_cache_capacity.unwrap_or(1);
let kv_cache_entry = KvCache {
input_id,
output_id,
cache: Some(KvCacheData::with_capacity(
batch_size,
n_heads,
size,
max_seq_len,
)),
};
if kv_pattern.encoder {
encoder_kv_cache.push(kv_cache_entry);
} else {
kv_cache.push(kv_cache_entry);
}
}
let mut generator = Generator {
model,
run_options: None,
constant_inputs: Vec::new(),
varying_inputs: Vec::new(),
constant_prop_inputs: Some(Vec::new()),
logits_filter: None,
input_ids: vec![],
input_ids_input,
input_offset: 0,
logits_output,
kv_cache,
encoder_kv_cache,
prev_tokens: Vec::new(),
sampler: Box::new(ArgMaxSampler {}),
};
let attention_mask_input = model.find_node(model_inputs.attention_mask);
if let Some(attention_mask_input) = attention_mask_input {
generator = generator
.with_varying_input(attention_mask_input, &|batch_size, positions| {
NdTensor::full([batch_size, positions.end], 1i32).into()
});
}
let position_ids_input = model.find_node(model_inputs.position_ids);
if let Some(position_ids_input) = position_ids_input {
generator =
generator.with_varying_input(position_ids_input, &|batch_size, positions| {
NdTensor::from_fn([batch_size, positions.len()], |[_batch, pos]| {
(positions.start + pos) as i32
})
.into()
});
}
let cache_position_input = model.find_node(model_inputs.cache_position);
if let Some(cache_position_input) = cache_position_input {
generator =
generator.with_varying_input(cache_position_input, &|_batch_size, positions| {
NdTensor::from_fn([positions.len()], |[pos]| (positions.start + pos) as i32)
.into()
});
}
let use_cache_input = model.find_node(model_inputs.use_cache_flag);
if let Some(use_cache_input) = use_cache_input {
generator = generator.with_varying_input(use_cache_input, &|_batch_size, positions| {
Tensor::from(if positions.start == 0 { 0i32 } else { 1 }).into()
});
}
Ok(generator)
}
pub fn with_prompt(mut self, prompt: &[TokenId]) -> Self {
self.input_ids = prompt.to_vec();
self
}
pub fn append_prompt(&mut self, prompt: &[TokenId]) {
self.input_ids.extend(prompt);
}
pub fn with_constant_input(mut self, input_id: NodeId, value: ValueView<'a>) -> Self {
self.constant_prop_inputs = None;
self.constant_inputs.push((input_id, value.into()));
self
}
pub fn with_varying_input<F: Fn(usize, Range<usize>) -> ValueOrView<'a>>(
mut self,
input_id: NodeId,
value_fn: &'a F,
) -> Self {
self.varying_inputs.push((input_id, value_fn));
self
}
pub fn with_logits_filter<F: LogitsFilter + 'a>(mut self, filter: F) -> Self {
self.logits_filter = Some(Box::new(filter));
self
}
pub fn with_sampler<S: Sampler + 'a>(mut self, sampler: S) -> Self {
self.sampler = Box::new(sampler);
self
}
pub fn with_run_options(mut self, opts: Option<RunOptions>) -> Self {
self.run_options = opts;
self
}
fn generate_next_token(&mut self) -> Result<TokenId, GeneratorError> {
fn wrap_error<E>(error: E, context: &str) -> GeneratorError
where
E: Into<Box<dyn Error>>,
{
let error_ctx = ErrorContext {
error: error.into(),
context: context.to_string(),
};
GeneratorError::GenerateError(error_ctx.into())
}
let batch_size = 1;
let input_ids: NdTensor<i32, 2> = self
.input_ids
.iter()
.map(|id| *id as i32)
.collect::<Tensor<_>>()
.into_shape([batch_size, self.input_ids.len()]);
let input_positions = self.input_offset..self.input_offset + self.input_ids.len();
let mut model_inputs: Vec<(NodeId, ValueOrView)> =
vec![(self.input_ids_input, input_ids.view().into())];
if self.constant_prop_inputs.is_none() {
let inputs = match self.model.partial_run(
self.constant_inputs.clone(),
&[self.logits_output],
self.run_options.clone(),
) {
Ok(inputs) => inputs,
Err(err) => {
return Err(wrap_error(
err,
"failed to partially evaluate model with constant inputs",
));
}
};
self.constant_prop_inputs = Some(inputs);
}
if let Some(constants) = self.constant_prop_inputs.as_ref() {
model_inputs.extend(
constants
.iter()
.map(|(node_id, output)| (*node_id, output.as_view().into())),
);
}
if !self.varying_inputs.is_empty() {
model_inputs.extend(self.varying_inputs.iter().map(|(node_id, value_fn)| {
(*node_id, value_fn(batch_size, input_positions.clone()))
}));
}
for entry in self.kv_cache.iter_mut() {
let cache = entry.cache.take();
match cache {
Some(KvCacheData::BatchSeqChans(cache)) => {
model_inputs.push((entry.input_id, cache.into()));
}
Some(KvCacheData::BatchHeadSeqChans(cache)) => {
model_inputs.push((entry.input_id, cache.into()));
}
None => {}
}
}
for entry in self.encoder_kv_cache.iter() {
match &entry.cache {
Some(KvCacheData::BatchSeqChans(cache)) => {
model_inputs.push((entry.input_id, cache.into()));
}
Some(KvCacheData::BatchHeadSeqChans(cache)) => {
model_inputs.push((entry.input_id, cache.into()));
}
None => {}
}
}
let model_outputs: Vec<NodeId> = [self.logits_output]
.into_iter()
.chain(self.kv_cache.iter().map(|entry| entry.output_id))
.chain(self.encoder_kv_cache.iter().map(|entry| entry.output_id))
.collect();
let mut outputs = self
.model
.run(model_inputs, &model_outputs, self.run_options.clone())
.map_err(|e| wrap_error(e, "failed to run model"))?;
if self.prev_tokens.is_empty() {
self.prev_tokens.extend(self.input_ids.iter());
}
let logits: NdTensor<f32, 3> = outputs
.remove(0)
.try_into()
.map_err(|e| wrap_error(e, "failed to extract logits from model outputs"))?;
let last_logits = logits.slice((0, -1));
let filtered_logits = self
.logits_filter
.as_ref()
.and_then(|f| f.filter(last_logits, &self.prev_tokens))
.map(|l| l.into_cow())
.unwrap_or(last_logits.as_cow());
let next_id = self.sampler.sample(filtered_logits.view());
for cache_entry in self.kv_cache.iter_mut() {
let output = outputs.remove(0);
let err_context = "failed to save self-attention KV-cache";
let mut kv_cache = match output.ndim() {
3 => KvCacheData::BatchSeqChans(
output.try_into().map_err(|e| wrap_error(e, err_context))?,
),
4 => KvCacheData::BatchHeadSeqChans(
output.try_into().map_err(|e| wrap_error(e, err_context))?,
),
ndim => {
return Err(wrap_error(
format!("KV cache has {} dims, expected 3 or 4", ndim),
err_context,
));
}
};
if !kv_cache.has_capacity(kv_cache.sequence_len() + 1) {
kv_cache = kv_cache.clone_with_capacity(kv_cache.sequence_len() * 2);
}
cache_entry.cache = Some(kv_cache);
}
for cache_entry in self.encoder_kv_cache.iter_mut() {
let output = outputs.remove(0);
if output.is_empty() {
continue;
}
let err_context = "failed to save cross-attention KV-cache";
let kv_cache = match output.ndim() {
3 => KvCacheData::BatchSeqChans(
output.try_into().map_err(|e| wrap_error(e, err_context))?,
),
4 => KvCacheData::BatchHeadSeqChans(
output.try_into().map_err(|e| wrap_error(e, err_context))?,
),
ndim => {
return Err(wrap_error(
format!("KV cache has {} dims, expected 3 or 4", ndim),
err_context,
));
}
};
cache_entry.cache = Some(kv_cache);
}
self.prev_tokens.push(next_id);
if !self.kv_cache.is_empty() {
self.input_offset += self.input_ids.len();
self.input_ids = vec![next_id];
} else {
self.input_ids.push(next_id);
}
Ok(next_id)
}
}
pub type GeneratorItem = Result<TokenId, GeneratorError>;
impl Iterator for Generator<'_> {
type Item = Result<TokenId, GeneratorError>;
fn next(&mut self) -> Option<Self::Item> {
Some(self.generate_next_token())
}
}
pub trait GeneratorUtils: Iterator<Item = GeneratorItem> + Sized {
fn stop_on_tokens<A: AsRef<[u32]>>(self, eos_tokens: A) -> impl Iterator<Item = GeneratorItem> {
self.take_while(move |tok| match tok {
Ok(tok_id) => !eos_tokens.as_ref().contains(tok_id),
_ => true,
})
}
#[cfg(feature = "text-decoder")]
fn decode(self, tokenizer: &Tokenizer) -> TextDecoder<'_, Self> {
TextDecoder::wrap(self, tokenizer)
}
fn profile(self, metrics: &mut Metrics) -> impl Iterator<Item = Self::Item> {
Profiler::wrap(self, metrics)
}
}
impl<I: Iterator<Item = GeneratorItem>> GeneratorUtils for I {}
struct Profiler<'a, G: Iterator> {
generator: G,
metrics: &'a mut Metrics,
}
impl<'a, G: Iterator> Profiler<'a, G> {
fn wrap(generator: G, metrics: &'a mut Metrics) -> Profiler<'a, G> {
Profiler { generator, metrics }
}
}
impl<G: Iterator> Iterator for Profiler<'_, G> {
type Item = G::Item;
fn next(&mut self) -> Option<Self::Item> {
let start = std::time::Instant::now();
let item = self.generator.next()?;
self.metrics.add_step_duration(start.elapsed());
Some(item)
}
}
#[cfg(test)]
mod tests {
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::error::Error;
use std::rc::Rc;
use rten::{Dimension, NodeId, RunOptions, Value, ValueOrView};
use rten_tensor::prelude::*;
use rten_tensor::{NdTensor, NdTensorView};
use super::{Generator, GeneratorUtils};
use crate::filter::LogitsFilter;
use crate::metrics::Metrics;
use crate::model::{Model, NodeInfo};
struct FakeModel {
nodes: Vec<NodeInfo>,
input_ids: Vec<NodeId>,
output_ids: Vec<NodeId>,
step: Cell<usize>,
outputs: Vec<HashMap<NodeId, Value>>,
inputs: RefCell<Vec<HashMap<NodeId, Value>>>,
run_opts: Cell<Option<RunOptions>>,
}
impl FakeModel {
fn with_inputs_and_outputs(inputs: &[NodeInfo], outputs: &[NodeInfo]) -> FakeModel {
let node_infos = [inputs, outputs].concat();
let input_ids = (0..inputs.len())
.map(|id| NodeId::from_u32(id as u32))
.collect();
let output_ids = (inputs.len()..(inputs.len() + outputs.len()))
.map(|id| NodeId::from_u32(id as u32))
.collect();
FakeModel {
input_ids,
output_ids,
nodes: node_infos,
step: Cell::new(0),
inputs: RefCell::new(vec![]),
outputs: vec![],
run_opts: Cell::new(None),
}
}
fn add_outputs(&mut self, outputs: HashMap<NodeId, Value>) {
self.outputs.push(outputs)
}
fn get_inputs(&self, step: usize, node_id: NodeId) -> Option<Value> {
self.inputs
.borrow()
.get(step)
.map(|step_inputs| step_inputs.get(&node_id))
.flatten()
.cloned()
}
}
impl Model for FakeModel {
fn find_node(&self, name: &str) -> Option<NodeId> {
self.nodes
.iter()
.position(|info| info.name() == name)
.map(|pos| NodeId::from_u32(pos as u32))
}
fn node_info(&self, id: NodeId) -> Option<NodeInfo> {
self.nodes.get(id.as_usize()).cloned()
}
fn input_ids(&self) -> &[NodeId] {
&self.input_ids
}
fn run(
&self,
inputs: Vec<(NodeId, ValueOrView)>,
outputs: &[NodeId],
opts: Option<RunOptions>,
) -> Result<Vec<Value>, Box<dyn Error>> {
if let Some((input_id, _)) = inputs.iter().find(|(id, _)| !self.input_ids.contains(id))
{
return Err(format!("invalid input ID {}", input_id).into());
}
for &expected_input in self.input_ids.iter() {
if !inputs.iter().any(|&(id, _)| id == expected_input) {
return Err(format!("missing input ID {}", expected_input).into());
}
}
if let Some(output_id) = outputs.iter().find(|id| !self.output_ids.contains(id)) {
return Err(format!("invalid output ID {}", output_id).into());
}
self.inputs.borrow_mut().push(
inputs
.into_iter()
.map(|(id, input_or_output)| (id, input_or_output.to_owned()))
.collect(),
);
let result = outputs
.iter()
.map(|id| {
let step_outputs = self
.outputs
.get(self.step.get())
.expect("outputs not specified for step");
step_outputs
.get(id)
.cloned()
.expect("invalid output node ID")
})
.collect();
self.step.set(self.step.get() + 1);
self.run_opts.set(opts);
Ok(result)
}
fn partial_run(
&self,
_inputs: Vec<(NodeId, ValueOrView)>,
_outputs: &[NodeId],
_opts: Option<RunOptions>,
) -> Result<Vec<(NodeId, Value)>, Box<dyn Error>> {
Ok(Vec::new())
}
}
fn generate_logits(n_vocab: usize, token_ids: &[u32]) -> NdTensor<f32, 3> {
let mut logits = NdTensor::zeros([1, token_ids.len(), n_vocab]);
for (idx, id) in token_ids.iter().copied().enumerate() {
logits[[0, idx, id as usize]] = 1.0;
}
logits
}
#[derive(Copy, Clone, PartialEq)]
struct TransformerParams {
n_layers: usize,
n_heads: usize,
n_embed: usize,
n_vocab: usize,
}
impl Default for TransformerParams {
fn default() -> Self {
Self {
n_layers: 5,
n_heads: 3,
n_vocab: 5,
n_embed: 8,
}
}
}
#[derive(Copy, Clone, PartialEq)]
enum KvCacheType {
Decoder,
EncoderDecoder,
}
fn fake_transformer_model(
params: TransformerParams,
kv_cache: Option<KvCacheType>,
prompt_len: usize,
output_token_ids: &[u32],
) -> FakeModel {
let TransformerParams {
n_layers,
n_heads,
n_vocab,
n_embed,
} = params;
let mut inputs = vec![
NodeInfo::from_name_shape("input_ids", &[]),
NodeInfo::from_name_shape("cache_position", &[]),
NodeInfo::from_name_shape("position_ids", &[]),
NodeInfo::from_name_shape("attention_mask", &[]),
];
let mut outputs = vec![NodeInfo::from_name_shape("logits", &[])];
let mut kv_cache_output_names = Vec::new();
if let Some(kv_cache_type) = kv_cache {
let dims = [
Dimension::Symbolic("batch".to_string()),
Dimension::Fixed(n_heads as usize),
Dimension::Symbolic("seq".to_string()),
Dimension::Fixed(n_embed),
];
let make_name_info = |name: &str| NodeInfo::from_name_shape(name, &dims);
for layer in 0..n_layers {
let past_names: Vec<String>;
let present_names: Vec<String>;
match kv_cache_type {
KvCacheType::Decoder => {
past_names = [
format!("past_key_values.{}.key", layer),
format!("past_key_values.{}.value", layer),
]
.into();
present_names = [
format!("present.{}.key", layer),
format!("present.{}.value", layer),
]
.into();
}
KvCacheType::EncoderDecoder => {
past_names = [
format!("past_key_values.{}.decoder.key", layer),
format!("past_key_values.{}.decoder.value", layer),
format!("past_key_values.{}.encoder.key", layer),
format!("past_key_values.{}.encoder.value", layer),
]
.into();
present_names = [
format!("present.{}.decoder.key", layer),
format!("present.{}.decoder.value", layer),
format!("present.{}.encoder.key", layer),
format!("present.{}.encoder.value", layer),
]
.into();
}
}
inputs.extend(past_names.iter().map(|name| make_name_info(&name)));
outputs.extend(present_names.iter().map(|name| make_name_info(&name)));
kv_cache_output_names.extend(present_names);
}
if kv_cache_type == KvCacheType::EncoderDecoder {
inputs.push(NodeInfo::from_name_shape("use_cache_branch", &[]));
}
}
let mut model = FakeModel::with_inputs_and_outputs(&inputs, &outputs);
let logits_id = model.find_node("logits").unwrap();
for (step, output_token_id) in output_token_ids.iter().copied().enumerate() {
assert!(
output_token_id < n_vocab as u32,
"token ID is invalid for vocab size"
);
let logits = if kv_cache.is_some() {
generate_logits(n_vocab, &[output_token_id])
} else {
generate_logits(n_vocab, &output_token_ids[..=step])
};
let mut outputs = HashMap::new();
outputs.insert(logits_id, Value::FloatTensor(logits.into()));
for kv_output in kv_cache_output_names.iter() {
let kv_output_id = model.find_node(&kv_output).unwrap();
let context_len = if step == 0 {
prompt_len
} else {
prompt_len + step - 1
};
let is_encoder = model
.node_info(kv_output_id)
.as_ref()
.map(|ni| ni.name())
.unwrap_or("")
.contains("encoder");
let output_n_embed = if is_encoder && step > 0 {
0
} else {
n_embed
};
outputs.insert(
kv_output_id,
Value::FloatTensor(
NdTensor::zeros([1, n_heads, context_len, output_n_embed]).into(),
),
);
}
model.add_outputs(outputs);
}
model
}
fn test_generator_impl(kv_cache_type: Option<KvCacheType>) -> Result<(), Box<dyn Error>> {
let params = TransformerParams::default();
let expected_token_ids = [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 0, 0];
let prompt = [1, 2, 3, 1, 2, 3];
let model =
fake_transformer_model(params, kv_cache_type, prompt.len(), &expected_token_ids);
let generator = Generator::from_model(&model)?;
let generation_len = 10;
let output_token_ids: Vec<_> = generator
.with_prompt(&prompt)
.take(generation_len)
.map(|id| id.expect("generation failed"))
.collect();
assert_eq!(output_token_ids.len(), generation_len);
assert_eq!(output_token_ids, &expected_token_ids[..generation_len]);
let input_id = model.find_node("input_ids").unwrap();
let position_ids = model.find_node("position_ids").unwrap();
let attention_mask = model.find_node("attention_mask").unwrap();
let cache_branch = model.find_node("use_cache_branch");
let cache_position = model.find_node("cache_position").unwrap();
for step in 0..generation_len {
let step_inputs = model.get_inputs(step, input_id).unwrap();
let step_inputs: NdTensor<i32, 2> = step_inputs.try_into().unwrap();
let step_pos_ids = model.get_inputs(step, position_ids).unwrap();
let step_pos_ids: NdTensor<i32, 2> = step_pos_ids.try_into().unwrap();
let step_cache_pos = model.get_inputs(step, cache_position).unwrap();
let step_cache_pos: NdTensor<i32, 1> = step_cache_pos.try_into().unwrap();
let step_attn_mask = model.get_inputs(step, attention_mask).unwrap();
let step_attn_mask: NdTensor<i32, 2> = step_attn_mask.try_into().unwrap();
let cache_branch = cache_branch.map(|cb_id| {
let cb = model.get_inputs(step, cb_id).unwrap();
let cb: NdTensor<i32, 0> = cb.try_into().unwrap();
cb
});
if step == 0 {
assert_eq!(step_inputs.size(1), prompt.len());
assert!(
step_inputs
.iter()
.map(|x| *x as u32)
.eq(prompt.iter().copied())
);
assert_eq!(step_attn_mask.size(1), prompt.len());
assert!(step_attn_mask.iter().all(|x| *x == 1));
assert_eq!(step_pos_ids.size(1), prompt.len());
assert!(step_pos_ids.iter().map(|x| *x as usize).eq(0..prompt.len()));
assert_eq!(step_cache_pos.size(0), prompt.len());
assert!(
step_cache_pos
.iter()
.map(|x| *x as usize)
.eq(0..prompt.len())
);
if let Some(cache_branch) = cache_branch {
assert_eq!(cache_branch.item(), Some(&0));
}
} else if kv_cache_type.is_some() {
assert_eq!(step_inputs.size(1), 1);
assert_eq!(step_inputs[[0, 0]] as u32, expected_token_ids[step - 1]);
assert_eq!(step_attn_mask.size(1), prompt.len() + step);
assert_eq!(step_attn_mask[[0, 0]], 1);
assert_eq!(step_pos_ids.size(1), 1);
assert_eq!(step_pos_ids[[0, 0]], (prompt.len() + step - 1) as i32);
assert_eq!(step_cache_pos.size(0), 1);
assert_eq!(step_cache_pos[[0]], (prompt.len() + step - 1) as i32);
if let Some(cache_branch) = cache_branch {
assert_eq!(cache_branch.item(), Some(&1));
}
} else {
let expected_inputs: Vec<i32> = prompt
.iter()
.copied()
.chain(expected_token_ids)
.take(prompt.len() + step)
.map(|x| x as i32)
.collect();
assert_eq!(
step_inputs,
NdTensor::from_data([1, expected_inputs.len()], expected_inputs)
);
let expected_attn_mask = vec![1i32; prompt.len() + step];
assert_eq!(
step_attn_mask,
NdTensor::from_data([1, expected_attn_mask.len()], expected_attn_mask)
);
let expected_pos_ids: Vec<i32> =
(0..prompt.len() + step).map(|x| x as i32).collect();
assert_eq!(
step_pos_ids,
NdTensor::from_data([1, expected_pos_ids.len()], expected_pos_ids.clone())
);
assert_eq!(
step_cache_pos,
NdTensor::from_data([expected_pos_ids.len()], expected_pos_ids)
);
}
}
Ok(())
}
#[test]
fn test_generator_with_decoder_kv_cache() -> Result<(), Box<dyn Error>> {
test_generator_impl(Some(KvCacheType::Decoder))
}
#[test]
fn test_generator_with_encoder_decoder_kv_cache() -> Result<(), Box<dyn Error>> {
test_generator_impl(Some(KvCacheType::EncoderDecoder))
}
#[test]
fn test_generator_without_kv_cache() -> Result<(), Box<dyn Error>> {
test_generator_impl(None)
}
#[test]
fn test_generator_append_prompt() -> Result<(), Box<dyn Error>> {
let mut params = TransformerParams::default();
params.n_vocab = 110;
let output_token_ids = [0, 1, 2, 3, 4, 5, 6, 7, 8];
let prompt = [99];
let model = fake_transformer_model(
params,
Some(KvCacheType::Decoder),
prompt.len(),
&output_token_ids,
);
let mut generator = Generator::from_model(&model)?.with_prompt(&prompt);
generator.next();
generator.append_prompt(&[100]);
generator.next();
generator.append_prompt(&[101, 102]);
generator.next();
let input_id = model.find_node("input_ids").unwrap();
let inputs = model.get_inputs(0, input_id).unwrap();
let inputs: NdTensor<i32, 2> = inputs.try_into().unwrap();
assert_eq!(inputs, NdTensor::from([[99]]));
let inputs = model.get_inputs(1, input_id).unwrap();
let inputs: NdTensor<i32, 2> = inputs.try_into().unwrap();
assert_eq!(inputs, NdTensor::from([[0, 100]]));
let inputs = model.get_inputs(2, input_id).unwrap();
let inputs: NdTensor<i32, 2> = inputs.try_into().unwrap();
assert_eq!(inputs, NdTensor::from([[1, 101, 102]]));
Ok(())
}
#[test]
fn test_stop_on_tokens() -> Result<(), Box<dyn Error>> {
let params = TransformerParams::default();
let expected_token_ids = [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 0, 0];
let prompt = [1, 2, 3, 1, 2, 3];
let model = fake_transformer_model(
params,
Some(KvCacheType::Decoder),
prompt.len(),
&expected_token_ids,
);
let generator = Generator::from_model(&model)?;
let output_token_ids: Vec<_> = generator
.with_prompt(&prompt)
.stop_on_tokens([4])
.map(|id| id.expect("generation failed"))
.collect();
assert_eq!(output_token_ids, &[0, 1, 2, 3]);
Ok(())
}
#[test]
fn test_profile() -> Result<(), Box<dyn Error>> {
let params = TransformerParams::default();
let expected_token_ids = [0, 1, 2, 3, 4];
let prompt = [1, 2, 3, 1, 2, 3];
let model = fake_transformer_model(
params,
Some(KvCacheType::Decoder),
prompt.len(),
&expected_token_ids,
);
let generator = Generator::from_model(&model)?;
let mut metrics = Metrics::new();
let output_token_ids: Vec<_> = generator
.with_prompt(&prompt)
.profile(&mut metrics)
.take(expected_token_ids.len())
.map(|id| id.expect("generation failed"))
.collect();
assert_eq!(output_token_ids, expected_token_ids);
assert!(metrics.warmup_duration().is_some());
assert_eq!(metrics.step_durations().len(), output_token_ids.len() - 1);
Ok(())
}
#[test]
fn test_filter() -> Result<(), Box<dyn Error>> {
let mut params = TransformerParams::default();
params.n_vocab = 8;
let expected_token_ids = [0, 1, 2, 3];
let prompt = [5, 6, 7];
let model = fake_transformer_model(
params,
Some(KvCacheType::Decoder),
prompt.len(),
&expected_token_ids,
);
let generator = Generator::from_model(&model)?;
struct DoubleIndexFilter {
prev_tokens: Rc<RefCell<Vec<u32>>>,
}
impl LogitsFilter for DoubleIndexFilter {
fn filter(
&self,
logits: NdTensorView<f32, 1>,
prev_tokens: &[u32],
) -> Option<NdTensor<f32, 1>> {
self.prev_tokens.replace(prev_tokens.to_vec());
let max_idx = logits
.iter()
.enumerate()
.max_by(|(_i, x), (_j, y)| x.total_cmp(y))
.map(|(i, _x)| i)?;
Some(NdTensor::from_fn(logits.shape(), |[i]| {
if i == max_idx * 2 { 1. } else { 0. }
}))
}
}
let prev_tokens = Rc::new(RefCell::new(Vec::new()));
let output_token_ids: Vec<_> = generator
.with_prompt(&prompt)
.with_logits_filter(DoubleIndexFilter {
prev_tokens: prev_tokens.clone(),
})
.take(expected_token_ids.len())
.map(|id| id.expect("generation failed"))
.collect();
assert_eq!(output_token_ids, [0, 2, 4, 6]);
assert_eq!(prev_tokens.borrow().as_slice(), [5, 6, 7, 0, 2, 4]);
Ok(())
}
#[test]
fn test_run_options() -> Result<(), Box<dyn Error>> {
let params = TransformerParams::default();
let expected_token_ids = [0, 1, 2, 3, 4];
let prompt = [1, 2, 3, 1, 2, 3];
let model = fake_transformer_model(
params,
Some(KvCacheType::Decoder),
prompt.len(),
&expected_token_ids,
);
let generator = Generator::from_model(&model)?;
let run_opts = RunOptions {
verbose: true,
..Default::default()
};
let output_token_ids: Vec<_> = generator
.with_prompt(&prompt)
.with_run_options(Some(run_opts.clone()))
.take(expected_token_ids.len())
.map(|id| id.expect("generation failed"))
.collect();
assert_eq!(output_token_ids, expected_token_ids);
assert_eq!(model.run_opts.take(), Some(run_opts));
Ok(())
}
}