use std::{path::Path, sync::Arc, time::Instant};
use half::f16;
use windit::plan::Span;
use super::{
super::{
alloc::{self, reserve},
artifacts::{
ArtifactLabel, Part,
stamp::{PartView, Stamp},
},
audio::AudioPrecision,
config::{self, BOA_ID, BOI_ID, EOA_ID, EOI_ID, EOS_ID, HIDDEN_SIZE, MAX_TOKENS, PACK_SLOTS},
embedding::{EMBEDDING_DIM, Embedding},
error::{
AllocationSite, Artifact, ContractRefusal, Error, Function, LoadRefusal, NonFiniteOutput,
PredictionFailure, Result,
},
long_text::{WindowEmbedding, fold_windows, plan as plan_windows},
media::{AudioTokens, ImageTokens},
options::{Batching, DerivationTerms, TextOptions},
prewarm::{PrewarmReport, Warmed},
prompt::Prompt,
table::TokenTable,
tokenizer::{
SplitSpecialTokens, UPSTREAM_SENTINELS,
bpe::{Encoded, TokenId, TokenizerScratch},
image::{TokenizerImage, UPSTREAM},
},
},
TextFunction, TextFunctions,
layout::{
MediaSequence, PACK_ROWS, PackedInputs, Sequence, choose, fill_packed, fill_unpacked,
plan_packs, unpacked,
},
};
use crate::{
DataType, FunctionName, Model, MultiArray,
embeddings::{aggregate::AggregatePolicyKind, long_text::LongTextOptions},
model::contract::{Checked, Dim, FeatureContract, LoadContract, StateContract},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Framing {
ids: usize,
conservative: bool,
}
impl Framing {
#[inline]
pub const fn ids(&self) -> usize {
self.ids
}
#[inline]
pub const fn is_conservative(&self) -> bool {
self.conservative
}
}
pub(crate) fn framing_table(
image: &TokenizerImage,
scratch: &mut TokenizerScratch,
) -> Result<[usize; 9]> {
let mut ids = Vec::new();
let mut table = [0; 9];
let texts = Prompt::NAMED.map(|(_, text)| text.strip_suffix(' ').unwrap_or(text));
for (slot, text) in texts.iter().chain(&[""]).enumerate() {
ids.clear();
image.encode_joint(
&Prompt::Bare,
text,
SplitSpecialTokens::Recognize,
scratch,
Encoded {
ids: &mut ids,
spans: None,
},
)?;
table[slot] = ids.len();
}
Ok(table)
}
pub(crate) fn framing_of(
image: &TokenizerImage,
table: &[usize; 9],
prompt: Prompt<'_>,
split: SplitSpecialTokens,
scratch: &mut TokenizerScratch,
) -> Result<Framing> {
let named = Prompt::NAMED
.iter()
.position(|(key, _)| prompt.config_key() == Some(*key));
if let Some(slot) = named {
return Ok(Framing {
ids: table[slot],
conservative: false,
});
}
let mut ids = Vec::new();
let mut encode = |prompt: Prompt<'_>, text: &str| -> Result<usize> {
ids.clear();
image.encode_joint(
&prompt,
text,
split,
scratch,
Encoded {
ids: &mut ids,
spans: None,
},
)?;
Ok(ids.len())
};
Ok(match prompt {
Prompt::Document(Some(title)) => Framing {
ids: 7 + encode(Prompt::Custom(" "), title)? - 2,
conservative: false,
},
Prompt::Custom(prefix) => Framing {
ids: encode(Prompt::Bare, prefix)?,
conservative: true,
},
_ => Framing {
ids: table[8],
conservative: false,
},
})
}
#[derive(Debug)]
struct Shared {
view: PartView,
table: TokenTable,
image: TokenizerImage,
framing: [usize; 9],
}
#[derive(Debug, Clone)]
pub struct TextArtifacts {
shared: Arc<Shared>,
}
impl TextArtifacts {
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
let dir = dir.as_ref();
let stamp = Stamp::read(dir)?;
let view = stamp.part(dir, Part::Text, AudioPrecision::Fp16)?;
config::load(&dir.join("config.json"))?;
let table = TokenTable::open(&dir.join("embeddings.bf16"), view.sentinels())?;
let image_name = &view
.image()
.expect("the stamp's text part names its tokenizer image")
.name;
let image = TokenizerImage::open(&dir.join(image_name), &UPSTREAM)?;
let mut scratch = TokenizerScratch::new();
image.check_sentinels(&UPSTREAM_SENTINELS, &mut scratch)?;
let framing = framing_table(&image, &mut scratch)?;
Ok(Self {
shared: Arc::new(Shared {
view,
table,
image,
framing,
}),
})
}
pub fn token_ids(
&self,
text: &str,
prompt: Prompt<'_>,
split: SplitSpecialTokens,
scratch: &mut TokenizerScratch,
ids: &mut Vec<TokenId>,
) -> Result<()> {
ids.clear();
self
.shared
.image
.encode_joint(&prompt, text, split, scratch, Encoded { ids, spans: None })
}
#[inline]
pub fn artifact_label(&self) -> ArtifactLabel {
self.shared.view.label()
}
pub fn framing(&self, prompt: Prompt<'_>, split: SplitSpecialTokens) -> Result<Framing> {
framing_of(
&self.shared.image,
&self.shared.framing,
prompt,
split,
&mut TokenizerScratch::new(),
)
}
#[cfg(test)]
pub(crate) fn image(&self) -> &TokenizerImage {
&self.shared.image
}
#[cfg(test)]
pub(crate) fn table(&self) -> &TokenTable {
&self.shared.table
}
#[cfg(test)]
pub(crate) fn same_mappings(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.shared, &other.shared)
}
}
fn contract(function: TextFunction) -> LoadContract {
let rows = function.tokens();
let f16 = |name, dims: &[usize]| {
FeatureContract::new(
name,
DataType::F16,
dims.iter().map(|&d| Dim::Exactly(d)).collect(),
)
};
let (inputs, batch) = match function {
TextFunction::Pack256 => (
vec![
f16("attention_bias", &[1, 1, PACK_ROWS, PACK_ROWS]),
f16("inputs_embeds", &[1, PACK_ROWS, HIDDEN_SIZE]),
f16("pool", &[PACK_SLOTS, PACK_ROWS]),
f16("positions", &[PACK_ROWS, 1]),
],
PACK_SLOTS,
),
_ => (
vec![
f16("attention_mask", &[1, rows]),
f16("inputs_embeds", &[1, rows, HIDDEN_SIZE]),
],
1,
),
};
LoadContract::new(
inputs,
vec![FeatureContract::new(
"embedding",
DataType::F32,
vec![Dim::Exactly(batch), Dim::Exactly(EMBEDDING_DIM)],
)],
StateContract::None,
)
}
#[derive(Debug)]
struct Loaded {
model: Checked,
inputs: Inputs,
}
#[derive(Debug)]
enum Inputs {
Embed(EmbedInputs),
Pack(PackInputs),
}
#[derive(Debug)]
struct EmbedInputs {
mask: MultiArray,
inputs_embeds: MultiArray,
}
#[derive(Debug)]
struct PackInputs {
attention_bias: MultiArray,
inputs_embeds: MultiArray,
pool: MultiArray,
positions: MultiArray,
}
impl Inputs {
fn new(function: TextFunction) -> Result<Self> {
let zeros = |shape: &[usize]| MultiArray::zeros(shape, DataType::F16);
Ok(match function {
TextFunction::Pack256 => Self::Pack(PackInputs {
attention_bias: zeros(&[1, 1, PACK_ROWS, PACK_ROWS])?,
inputs_embeds: zeros(&[1, PACK_ROWS, HIDDEN_SIZE])?,
pool: zeros(&[PACK_SLOTS, PACK_ROWS])?,
positions: zeros(&[PACK_ROWS, 1])?,
}),
_ => Self::Embed(EmbedInputs {
mask: zeros(&[1, function.tokens()])?,
inputs_embeds: zeros(&[1, function.tokens(), HIDDEN_SIZE])?,
}),
})
}
}
#[derive(Debug)]
pub struct TextEmbedder {
artifacts: TextArtifacts,
options: TextOptions,
functions: [Option<Loaded>; 7],
scratch: TokenizerScratch,
ids: Vec<TokenId>,
media: Vec<f16>,
}
const fn slot(function: TextFunction) -> usize {
function as usize
}
impl TextEmbedder {
pub fn new(artifacts: &TextArtifacts, options: &TextOptions) -> Result<Self> {
artifacts.shared.view.expect(options.expect_artifacts())?;
Ok(Self {
artifacts: artifacts.clone(),
options: options.clone(),
functions: Default::default(),
scratch: TokenizerScratch::new(),
ids: Vec::new(),
media: Vec::new(),
})
}
#[inline]
pub fn options(&self) -> &TextOptions {
&self.options
}
#[inline]
pub fn derivation_terms(&self) -> DerivationTerms<'_> {
DerivationTerms::text(self.artifacts.artifact_label(), &self.options)
}
#[cfg(test)]
pub(crate) fn loaded(&self) -> TextFunctions {
TextFunction::ALL
.into_iter()
.filter(|&function| self.functions[slot(function)].is_some())
.collect()
}
pub fn prewarm_set(&self) -> TextFunctions {
self.options.prewarm().unwrap_or_else(|| {
let set: TextFunctions = TextFunction::ALL
.into_iter()
.filter(|f| *f != TextFunction::Pack256)
.collect();
match self.options.batching() {
Batching::Packed => set.with(TextFunction::Pack256),
Batching::Single => set,
}
})
}
pub fn prewarm(&mut self) -> Result<PrewarmReport> {
let mut report = PrewarmReport::default();
let bos_eos = [
TokenId::from_vocab(config::BOS_ID),
TokenId::from_vocab(EOS_ID),
];
for function in self.prewarm_set().iter() {
let started = Instant::now();
self.load(function)?;
let load = started.elapsed();
let started = Instant::now();
let sequence = [Sequence::Text(&bos_eos)];
self.predict(function, &sequence, &mut |_, _| Ok(()))?;
report.push(Warmed::new(
Some(Function::Text(function)),
load,
started.elapsed(),
));
}
Ok(report)
}
pub fn embed(&mut self, text: &str, prompt: Prompt<'_>) -> Result<Embedding> {
let mut out = Vec::new();
self.embed_batch(&[text], prompt, &mut out)?;
Ok(out.pop().expect("one text, one vector"))
}
pub fn embed_batch(
&mut self,
texts: &[&str],
prompt: Prompt<'_>,
out: &mut Vec<Embedding>,
) -> Result<()> {
self.embed_batch_with(texts, prompt, self.options.batching(), out)
}
pub fn embed_batch_with(
&mut self,
texts: &[&str],
prompt: Prompt<'_>,
batching: Batching,
out: &mut Vec<Embedding>,
) -> Result<()> {
let split = self.options.split();
let mut encoded: Vec<Vec<TokenId>> =
alloc::with_capacity(texts.len(), AllocationSite::TokenIds)?;
for text in texts {
self
.artifacts
.token_ids(text, prompt, split, &mut self.scratch, &mut self.ids)?;
if self.ids.len() > MAX_TOKENS {
self.ids.truncate(MAX_TOKENS - 1);
self.ids.push(TokenId::from_vocab(EOS_ID));
}
encoded.push(core::mem::take(&mut self.ids));
}
let sequences = alloc::collect(
encoded.iter().map(|ids| Sequence::Text(ids)),
AllocationSite::BatchOutput,
)?;
self.embed_sequences(&sequences, batching, out)
}
pub fn embed_long(&mut self, text: &str, prompt: Prompt<'_>) -> Result<Embedding> {
let (geometry, fold) = (self.options.long_text(), self.options.fold());
self.embed_long_with(text, prompt, &geometry, &fold)
}
pub fn embed_windows(&mut self, text: &str, prompt: Prompt<'_>) -> Result<Vec<WindowEmbedding>> {
let geometry = self.options.long_text();
self.embed_windows_with(text, prompt, &geometry)
}
pub fn embed_long_with(
&mut self,
text: &str,
prompt: Prompt<'_>,
geometry: &LongTextOptions,
fold: &AggregatePolicyKind,
) -> Result<Embedding> {
let windows = self.embed_windows_with(text, prompt, geometry)?;
fold_windows(windows, fold)
}
pub fn embed_windows_with(
&mut self,
text: &str,
prompt: Prompt<'_>,
geometry: &LongTextOptions,
) -> Result<Vec<WindowEmbedding>> {
let split = self.options.split();
let framing = self.artifacts.framing(prompt, split)?;
let planned = plan_windows(
&self.artifacts.shared.image,
text,
prompt,
split,
geometry,
framing,
&mut self.scratch,
)?;
let sequences = alloc::collect(
planned.iter().map(|window| Sequence::Text(&window.ids)),
AllocationSite::WindowList,
)?;
let mut vectors = Vec::new();
self.embed_sequences(&sequences, self.options.batching(), &mut vectors)?;
let mut windows = alloc::with_capacity(planned.len(), AllocationSite::WindowList)?;
let mut offset = 0;
for (ordinal, (window, embedding)) in planned.into_iter().zip(vectors).enumerate() {
let span = Span::try_new(offset, window.ids.len(), MAX_TOKENS)?;
offset += window.ids.len();
windows.push(WindowEmbedding::new(ordinal, window.range, span, embedding));
}
Ok(windows)
}
pub fn embed_audio_tokens(&mut self, tokens: &AudioTokens) -> Result<Embedding> {
let mut out = Vec::new();
self.embed_audio_tokens_batch_with(&[tokens], Batching::Single, &mut out)?;
Ok(out.pop().expect("one window, one vector"))
}
pub fn embed_image_tokens(&mut self, tokens: &ImageTokens) -> Result<Embedding> {
let mut out = Vec::new();
self.embed_image_tokens_batch_with(&[tokens], Batching::Single, &mut out)?;
Ok(out.pop().expect("one image, one vector"))
}
pub fn embed_audio_tokens_batch_with(
&mut self,
tokens: &[&AudioTokens],
batching: Batching,
out: &mut Vec<Embedding>,
) -> Result<()> {
let mut rows: Vec<Vec<f16>> = alloc::with_capacity(tokens.len(), AllocationSite::BatchOutput)?;
for window in tokens {
window.rows_into(&mut self.media)?;
rows.push(core::mem::take(&mut self.media));
}
let sequences = alloc::collect(
rows.iter().map(|rows| {
Sequence::Media(MediaSequence {
open: TokenId::from_vocab(BOA_ID),
rows,
close: TokenId::from_vocab(EOA_ID),
})
}),
AllocationSite::BatchOutput,
)?;
self.embed_sequences(&sequences, batching, out)
}
pub fn embed_image_tokens_batch_with(
&mut self,
tokens: &[&ImageTokens],
batching: Batching,
out: &mut Vec<Embedding>,
) -> Result<()> {
let mut rows: Vec<Vec<f16>> = alloc::with_capacity(tokens.len(), AllocationSite::BatchOutput)?;
for image in tokens {
image.rows_into(&mut self.media)?;
rows.push(core::mem::take(&mut self.media));
}
let sequences = alloc::collect(
rows.iter().map(|rows| {
Sequence::Media(MediaSequence {
open: TokenId::from_vocab(BOI_ID),
rows,
close: TokenId::from_vocab(EOI_ID),
})
}),
AllocationSite::BatchOutput,
)?;
self.embed_sequences(&sequences, batching, out)
}
fn embed_sequences(
&mut self,
sequences: &[Sequence<'_>],
batching: Batching,
out: &mut Vec<Embedding>,
) -> Result<()> {
reserve(out, sequences.len(), AllocationSite::BatchOutput)?;
let mut vectors: Vec<Option<Embedding>> = alloc::collect(
(0..sequences.len()).map(|_| None),
AllocationSite::BatchOutput,
)?;
match batching {
Batching::Single => {
for function in TextFunction::ALL {
let members = (0..sequences.len())
.filter(|&i| choose(sequences[i].len(), Batching::Single) == function);
for index in members {
self.predict(function, &sequences[index..=index], &mut |_, vector| {
vectors[index] = Some(vector);
Ok(())
})?;
}
}
}
Batching::Packed => {
let lengths = alloc::collect(
sequences.iter().map(Sequence::len),
AllocationSite::PackLayout,
)?;
let (packs, alone) = plan_packs(&lengths)?;
for pack in &packs {
let members = alloc::collect(
pack.members().iter().map(|&i| sequences[i]),
AllocationSite::PackLayout,
)?;
self.predict(TextFunction::Pack256, &members, &mut |slot, vector| {
vectors[pack.members()[slot]] = Some(vector);
Ok(())
})?;
}
for index in alone {
let function = unpacked(sequences[index].len());
self.predict(function, &sequences[index..=index], &mut |_, vector| {
vectors[index] = Some(vector);
Ok(())
})?;
}
}
}
out.extend(
vectors
.into_iter()
.map(|v| v.expect("every sequence is embedded once")),
);
Ok(())
}
fn load(&mut self, function: TextFunction) -> Result<&mut Loaded> {
let index = slot(function);
if self.functions[index].is_none() {
let name = FunctionName::new(function.as_str()).expect("a function name is not empty");
let model = Model::load_function_with(
self.artifacts.shared.view.bundle(),
name,
&self.options.coreml(),
)
.map_err(|e| {
Error::Load(LoadRefusal::new(
Artifact::TextBundle,
Some(Function::Text(function)),
e,
))
})?;
let model = Checked::new(model, &contract(function)).map_err(|violation| {
ContractRefusal::of(
Artifact::TextBundle,
Some(Function::Text(function)),
violation,
)
})?;
self.functions[index] = Some(Loaded {
model,
inputs: Inputs::new(function)?,
});
}
Ok(self.functions[index].as_mut().expect("loaded just above"))
}
fn predict(
&mut self,
function: TextFunction,
sequences: &[Sequence<'_>],
each: &mut dyn FnMut(usize, Embedding) -> Result<()>,
) -> Result<()> {
let table = self.artifacts.shared.table.clone();
let loaded = self.load(function)?;
let features = match &mut loaded.inputs {
Inputs::Embed(EmbedInputs {
mask,
inputs_embeds,
}) => {
fill_unpacked(
&table,
&sequences[0],
inputs_embeds.as_slice_mut::<f16>()?,
mask.as_slice_mut::<f16>()?,
)?;
loaded.model.predict_with(&[
("attention_mask", &*mask),
("inputs_embeds", &*inputs_embeds),
])
}
Inputs::Pack(PackInputs {
attention_bias,
inputs_embeds,
pool,
positions,
}) => {
fill_packed(
&table,
sequences,
PackedInputs {
attention_bias: attention_bias.as_slice_mut::<f16>()?,
inputs_embeds: inputs_embeds.as_slice_mut::<f16>()?,
pool: pool.as_slice_mut::<f16>()?,
positions: positions.as_slice_mut::<f16>()?,
},
)?;
loaded.model.predict_with(&[
("attention_bias", &*attention_bias),
("inputs_embeds", &*inputs_embeds),
("pool", &*pool),
("positions", &*positions),
])
}
};
let failure = |e| {
Error::Prediction(PredictionFailure::new(
Artifact::TextBundle,
Some(Function::Text(function)),
e,
))
};
let mut features = features.map_err(failure)?;
let output = features.take("embedding").ok_or_else(|| {
failure(crate::PredictionError::MissingOutput(
"embedding".to_owned(),
))
})?;
let batch = output.shape()[0];
debug_assert!(
batch <= PACK_SLOTS,
"a text function returns at most {PACK_SLOTS} rows"
);
let mut held = [0f32; PACK_SLOTS * EMBEDDING_DIM];
let rows = &mut held[..batch.min(PACK_SLOTS) * EMBEDDING_DIM];
output.copy_into::<f32>(rows)?;
for (slot, row) in rows
.as_chunks::<EMBEDDING_DIM>()
.0
.iter()
.take(sequences.len())
.enumerate()
{
let vector = Embedding::from_output(row).ok_or_else(|| {
let index = row.iter().position(|v| !v.is_finite()).unwrap_or(0);
Error::NonFiniteOutput(NonFiniteOutput::new(
Artifact::TextBundle,
Some(Function::Text(function)),
slot * EMBEDDING_DIM + index,
))
})?;
each(slot, vector)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests;