use std::collections::{btree_map::Entry, BTreeMap};
use serde::{Deserialize, Serialize};
pub const MODEL_LOGITS_OBSERVATION_PATH: &str = "model.logits";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "dtype", content = "values", rename_all = "snake_case")]
pub enum TensorObservationData {
F32(Vec<f32>),
I64(Vec<i64>),
U64(Vec<u64>),
Bool(Vec<bool>),
}
impl TensorObservationData {
pub fn len(&self) -> usize {
match self {
Self::F32(values) => values.len(),
Self::I64(values) => values.len(),
Self::U64(values) => values.len(),
Self::Bool(values) => values.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TensorObservation {
shape: Vec<usize>,
data: TensorObservationData,
}
impl TensorObservation {
pub fn new(shape: Vec<usize>, data: TensorObservationData) -> Result<Self, ObservationError> {
let elements = shape.iter().try_fold(1usize, |count, dimension| {
count
.checked_mul(*dimension)
.ok_or(ObservationError::ShapeOverflow)
})?;
if elements != data.len() {
return Err(ObservationError::ElementCount {
shape,
expected: elements,
actual: data.len(),
});
}
Ok(Self { shape, data })
}
pub fn shape(&self) -> &[usize] {
&self.shape
}
pub const fn data(&self) -> &TensorObservationData {
&self.data
}
pub fn into_parts(self) -> (Vec<usize>, TensorObservationData) {
(self.shape, self.data)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ObservationValue {
Tensor(TensorObservation),
Float(f64),
Integer(i64),
Unsigned(u64),
Boolean(bool),
Text(String),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ObservationSet {
values: BTreeMap<String, ObservationValue>,
}
impl ObservationSet {
pub const fn new() -> Self {
Self {
values: BTreeMap::new(),
}
}
pub fn insert(
&mut self,
path: impl Into<String>,
value: ObservationValue,
) -> Result<(), ObservationError> {
let path = path.into();
if path.is_empty() {
return Err(ObservationError::EmptyPath);
}
match self.values.entry(path) {
Entry::Vacant(entry) => {
entry.insert(value);
}
Entry::Occupied(entry) => {
return Err(ObservationError::DuplicatePath(entry.key().clone()));
}
}
Ok(())
}
pub fn get(&self, path: &str) -> Option<&ObservationValue> {
self.values.get(path)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &ObservationValue)> {
self.values
.iter()
.map(|(path, value)| (path.as_str(), value))
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn prefixed(self, prefix: &str) -> Result<Self, ObservationError> {
if prefix.is_empty() {
return Ok(self);
}
let mut output = Self::new();
for (path, value) in self.values {
output.insert(format!("{prefix}.{path}"), value)?;
}
Ok(output)
}
pub fn extend(&mut self, other: Self) -> Result<(), ObservationError> {
for (path, value) in other.values {
self.insert(path, value)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "match", content = "path", rename_all = "snake_case")]
pub enum ObservationSelector {
Exact(String),
Prefix(String),
}
impl ObservationSelector {
pub fn matches(&self, path: &str) -> bool {
match self {
Self::Exact(expected) => path == expected,
Self::Prefix(prefix) => {
path == prefix
|| path
.strip_prefix(prefix)
.is_some_and(|suffix| suffix.starts_with('.'))
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ObservationRequest {
selectors: Vec<ObservationSelector>,
}
impl ObservationRequest {
pub const fn all() -> Self {
Self {
selectors: Vec::new(),
}
}
pub fn selected(selectors: impl IntoIterator<Item = ObservationSelector>) -> Self {
Self {
selectors: selectors.into_iter().collect(),
}
}
pub fn matches(&self, path: &str) -> bool {
self.selectors.is_empty() || self.selectors.iter().any(|selector| selector.matches(path))
}
pub fn selectors(&self) -> &[ObservationSelector] {
&self.selectors
}
}
#[derive(Debug)]
pub struct InspectedOutput<O> {
pub output: O,
pub observations: ObservationSet,
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
pub enum ObservationError {
#[error("tensor observation shape element count overflowed")]
ShapeOverflow,
#[error(
"tensor observation shape {shape:?} requires {expected} values, but received {actual}"
)]
ElementCount {
shape: Vec<usize>,
expected: usize,
actual: usize,
},
#[error("observation path must not be empty")]
EmptyPath,
#[error("duplicate observation path {0:?}")]
DuplicatePath(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tensor_shape_and_values_must_agree() {
let tensor = TensorObservation::new(
vec![2, 2],
TensorObservationData::F32(vec![1.0, 2.0, 3.0, 4.0]),
)
.unwrap();
assert_eq!(tensor.shape(), [2, 2]);
assert!(matches!(tensor.data(), TensorObservationData::F32(_)));
assert!(matches!(
TensorObservation::new(vec![2], TensorObservationData::I64(vec![1])),
Err(ObservationError::ElementCount { .. })
));
}
#[test]
fn selectors_and_sets_are_stable_and_collision_safe() {
let request = ObservationRequest::selected([
ObservationSelector::Exact(MODEL_LOGITS_OBSERVATION_PATH.into()),
ObservationSelector::Prefix("model.layers.2".into()),
]);
assert_eq!(MODEL_LOGITS_OBSERVATION_PATH, "model.logits");
assert!(request.matches(MODEL_LOGITS_OBSERVATION_PATH));
assert!(request.matches("model.layers.2.output"));
assert!(!request.matches("model.layers.20.output"));
let mut set = ObservationSet::new();
set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(3))
.unwrap();
assert_eq!(
set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(4)),
Err(ObservationError::DuplicatePath(
MODEL_LOGITS_OBSERVATION_PATH.into()
))
);
}
}