1use std::collections::BTreeMap;
4
5use eredu_core::{
6 CapabilityError, InputExtent, InputMetadataKey, InputModality, InputPartDescriptor,
7 InputPayloadKind, InputTensorIdentity, PreparedInputError, PreparedInputIdentity,
8};
9use sha2::{Digest, Sha256};
10
11pub trait PreparedInputInspector<Tensor> {
17 fn identity(&self, tensor: &Tensor) -> Result<InputTensorIdentity, PreparedInputError>;
19
20 fn i32_values(&self, tensor: &Tensor) -> Result<Vec<i32>, CapabilityError>;
22
23 fn bool_values(&self, tensor: &Tensor) -> Result<Vec<bool>, CapabilityError>;
25}
26
27#[derive(Debug, Clone, Eq, PartialEq)]
29#[non_exhaustive]
30pub enum PreparedInputPayload<Tensor> {
31 TokenIds(Tensor),
33 Tensor(Tensor),
35 Embeddings(Tensor),
37}
38
39impl<Tensor> PreparedInputPayload<Tensor> {
40 pub const fn kind(&self) -> InputPayloadKind {
42 match self {
43 Self::TokenIds(_) => InputPayloadKind::TokenIds,
44 Self::Tensor(_) => InputPayloadKind::Tensor,
45 Self::Embeddings(_) => InputPayloadKind::Embeddings,
46 }
47 }
48
49 pub const fn value(&self) -> &Tensor {
51 match self {
52 Self::TokenIds(value) | Self::Tensor(value) | Self::Embeddings(value) => value,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Eq, PartialEq)]
59pub struct PreparedInputPart<Tensor> {
60 modality: InputModality,
61 payload: PreparedInputPayload<Tensor>,
62 metadata: BTreeMap<InputMetadataKey, Tensor>,
63 extents: Vec<InputExtent>,
64}
65
66impl<Tensor> PreparedInputPart<Tensor> {
67 pub fn new(
69 modality: InputModality,
70 payload: PreparedInputPayload<Tensor>,
71 metadata: impl IntoIterator<Item = (InputMetadataKey, Tensor)>,
72 ) -> Result<Self, PreparedInputError> {
73 Self::new_with_extents(modality, payload, metadata, [])
74 }
75
76 pub fn new_with_extents(
78 modality: InputModality,
79 payload: PreparedInputPayload<Tensor>,
80 metadata: impl IntoIterator<Item = (InputMetadataKey, Tensor)>,
81 extents: impl IntoIterator<Item = InputExtent>,
82 ) -> Result<Self, PreparedInputError> {
83 let payload_kind = payload.kind();
84 if !payload_kind.accepts(modality) {
85 return Err(PreparedInputError::IncompatiblePayload {
86 modality,
87 payload: payload_kind,
88 });
89 }
90 let mut typed_metadata = BTreeMap::new();
91 for (key, value) in metadata {
92 if !key.accepts(modality) {
93 return Err(PreparedInputError::IncompatibleMetadata { modality, key });
94 }
95 if typed_metadata.insert(key, value).is_some() {
96 return Err(PreparedInputError::DuplicateMetadata { key });
97 }
98 }
99 let extents = extents.into_iter().collect::<Vec<_>>();
100 for (index, extent) in extents.iter().copied().enumerate() {
101 if !extent.accepts(modality) {
102 return Err(PreparedInputError::IncompatibleExtent { modality, extent });
103 }
104 if extents[..index]
105 .iter()
106 .any(|prior| std::mem::discriminant(prior) == std::mem::discriminant(&extent))
107 {
108 return Err(PreparedInputError::DuplicateExtent { extent });
109 }
110 }
111 Ok(Self {
112 modality,
113 payload,
114 metadata: typed_metadata,
115 extents,
116 })
117 }
118
119 pub const fn modality(&self) -> InputModality {
121 self.modality
122 }
123
124 pub const fn payload(&self) -> &PreparedInputPayload<Tensor> {
126 &self.payload
127 }
128
129 pub const fn metadata(&self) -> &BTreeMap<InputMetadataKey, Tensor> {
131 &self.metadata
132 }
133
134 pub fn metadata_value(&self, key: InputMetadataKey) -> Option<&Tensor> {
136 self.metadata.get(&key)
137 }
138
139 pub fn extents(&self) -> &[InputExtent] {
141 &self.extents
142 }
143
144 pub fn descriptor(
146 &self,
147 describe: &impl Fn(&Tensor) -> Result<InputTensorIdentity, PreparedInputError>,
148 ) -> Result<InputPartDescriptor, PreparedInputError> {
149 InputPartDescriptor::new_with_extents(
150 self.modality,
151 self.payload.kind(),
152 describe(self.payload.value())?,
153 self.metadata
154 .iter()
155 .map(|(key, value)| Ok((*key, describe(value)?)))
156 .collect::<Result<Vec<_>, PreparedInputError>>()?,
157 self.extents.iter().copied(),
158 )
159 }
160}
161
162#[derive(Debug, Clone, Eq, PartialEq)]
167pub struct PreparedModelInput<Tensor> {
168 parts: Vec<PreparedInputPart<Tensor>>,
169 identity: PreparedInputIdentity,
170}
171
172#[derive(Debug, Clone, Eq, PartialEq)]
178pub struct PreparedInputCacheIdentity {
179 prepared: PreparedInputIdentity,
180 semantic_content_fingerprint: String,
181 prefix_content_fingerprint: String,
182}
183
184impl PreparedInputCacheIdentity {
185 pub fn new(
187 prepared: PreparedInputIdentity,
188 semantic_content_fingerprint: impl Into<String>,
189 ) -> Result<Self, PreparedInputCacheIdentityError> {
190 let semantic_content_fingerprint = semantic_content_fingerprint.into();
191 if semantic_content_fingerprint.trim().is_empty() {
192 return Err(PreparedInputCacheIdentityError::EmptySemanticContent);
193 }
194 let words = prepared
195 .encode_words()
196 .map_err(PreparedInputCacheIdentityError::Prepared)?;
197 let mut digest = Sha256::new();
198 digest.update(b"eredu-prepared-input-cache-v1\0");
199 digest.update((words.len() as u64).to_le_bytes());
200 for word in words {
201 digest.update(word.to_le_bytes());
202 }
203 digest.update((semantic_content_fingerprint.len() as u64).to_le_bytes());
204 digest.update(semantic_content_fingerprint.as_bytes());
205 let prefix_content_fingerprint = digest
206 .finalize()
207 .iter()
208 .map(|byte| format!("{byte:02x}"))
209 .collect();
210 Ok(Self {
211 prepared,
212 semantic_content_fingerprint,
213 prefix_content_fingerprint,
214 })
215 }
216
217 pub const fn prepared(&self) -> &PreparedInputIdentity {
219 &self.prepared
220 }
221
222 pub fn semantic_content_fingerprint(&self) -> &str {
224 &self.semantic_content_fingerprint
225 }
226
227 pub fn prefix_content_fingerprint(&self) -> &str {
229 &self.prefix_content_fingerprint
230 }
231
232 pub fn logical_metadata_bytes(&self) -> Option<u64> {
235 u64::try_from(std::mem::size_of::<Self>())
236 .ok()?
237 .checked_add(self.prepared.logical_metadata_bytes()?)?
238 .checked_add(u64::try_from(self.semantic_content_fingerprint.len()).ok()?)?
239 .checked_add(u64::try_from(self.prefix_content_fingerprint.len()).ok()?)
240 }
241}
242
243#[derive(Debug, thiserror::Error)]
245pub enum PreparedInputCacheIdentityError {
246 #[error("prepared-input semantic content fingerprint must not be empty")]
248 EmptySemanticContent,
249 #[error("prepared-input cache identity is invalid: {0}")]
251 Prepared(PreparedInputError),
252}
253
254impl<Tensor> PreparedModelInput<Tensor> {
255 pub fn new(
257 parts: Vec<PreparedInputPart<Tensor>>,
258 describe: impl Fn(&Tensor) -> Result<InputTensorIdentity, PreparedInputError>,
259 ) -> Result<Self, PreparedInputError> {
260 let identity = PreparedInputIdentity::new(
261 parts
262 .iter()
263 .map(|part| part.descriptor(&describe))
264 .collect::<Result<Vec<_>, _>>()?,
265 )?;
266 Ok(Self { parts, identity })
267 }
268
269 pub const fn identity(&self) -> &PreparedInputIdentity {
271 &self.identity
272 }
273
274 pub fn cache_identity(
276 &self,
277 semantic_content_fingerprint: impl Into<String>,
278 ) -> Result<PreparedInputCacheIdentity, PreparedInputCacheIdentityError> {
279 PreparedInputCacheIdentity::new(self.identity.clone(), semantic_content_fingerprint)
280 }
281
282 pub fn parts(&self) -> &[PreparedInputPart<Tensor>] {
284 &self.parts
285 }
286
287 pub fn len(&self) -> usize {
289 self.parts.len()
290 }
291
292 pub fn is_empty(&self) -> bool {
294 self.parts.is_empty()
295 }
296
297 pub fn wire_values(&self) -> Vec<&Tensor> {
299 let mut values = Vec::new();
300 for part in &self.parts {
301 values.push(part.payload.value());
302 values.extend(part.metadata.values());
303 }
304 values
305 }
306
307 pub fn from_identity_wire_values(
309 identity: PreparedInputIdentity,
310 values: Vec<Tensor>,
311 describe: impl Fn(&Tensor) -> Result<InputTensorIdentity, PreparedInputError>,
312 ) -> Result<Self, PreparedInputError> {
313 let expected_values = identity
314 .parts()
315 .iter()
316 .map(|part| 1 + part.metadata().len())
317 .sum::<usize>();
318 if values.len() != expected_values {
319 return Err(PreparedInputError::WireValueCount {
320 expected: expected_values,
321 actual: values.len(),
322 });
323 }
324 let mut values = values.into_iter();
325 let mut parts = Vec::with_capacity(identity.len());
326 for descriptor in identity.parts() {
327 let payload = values.next().expect("validated prepared-input value count");
328 let payload = match descriptor.payload_kind() {
329 InputPayloadKind::TokenIds => PreparedInputPayload::TokenIds(payload),
330 InputPayloadKind::Tensor => PreparedInputPayload::Tensor(payload),
331 InputPayloadKind::Embeddings => PreparedInputPayload::Embeddings(payload),
332 payload_kind => {
333 return Err(PreparedInputError::IncompatiblePayload {
334 modality: descriptor.modality(),
335 payload: payload_kind,
336 });
337 }
338 };
339 let metadata = descriptor
340 .metadata()
341 .keys()
342 .copied()
343 .map(|key| {
344 (
345 key,
346 values.next().expect("validated prepared-input value count"),
347 )
348 })
349 .collect::<Vec<_>>();
350 parts.push(PreparedInputPart::new_with_extents(
351 descriptor.modality(),
352 payload,
353 metadata,
354 descriptor.extents(),
355 )?);
356 }
357 let actual = Self::new(parts, describe)?;
358 if actual.identity != identity {
359 return Err(PreparedInputError::WireIdentityMismatch);
360 }
361 Ok(actual)
362 }
363
364 pub fn into_parts(self) -> Vec<PreparedInputPart<Tensor>> {
366 self.parts
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use eredu_core::{checkpoint::TensorDtype, PreparedInputError};
373
374 use super::*;
375
376 #[derive(Debug, Clone, Eq, PartialEq)]
377 struct FakeTensor {
378 dtype: TensorDtype,
379 shape: Vec<usize>,
380 marker: u8,
381 }
382
383 fn fake(dtype: TensorDtype, shape: &[usize], marker: u8) -> FakeTensor {
384 FakeTensor {
385 dtype,
386 shape: shape.to_vec(),
387 marker,
388 }
389 }
390
391 fn describe(value: &FakeTensor) -> Result<InputTensorIdentity, PreparedInputError> {
392 InputTensorIdentity::new(value.dtype.clone(), value.shape.clone())
393 }
394
395 #[test]
396 fn composite_input_extension_binds_typed_parts_to_a_multi_group_graph() {
397 let graph = crate::ExecutionGraph::new(
398 vec![
399 crate::ExecutionGroupSpec::root("vision"),
400 crate::ExecutionGroupSpec::with_dependencies("text", ["vision"]),
401 ],
402 "text",
403 )
404 .unwrap();
405 let input = PreparedModelInput::new(
406 vec![
407 PreparedInputPart::new(
408 InputModality::Text,
409 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
410 [],
411 )
412 .unwrap(),
413 PreparedInputPart::new_with_extents(
414 InputModality::Image,
415 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[4, 12], 2)),
416 [(
417 InputMetadataKey::PatchGrid,
418 fake(TensorDtype::I32, &[1, 3], 3),
419 )],
420 [InputExtent::PatchGrid {
421 time: 1,
422 height: 2,
423 width: 2,
424 }],
425 )
426 .unwrap(),
427 ],
428 describe,
429 )
430 .unwrap();
431 let identity = input.identity().clone();
432 let values = input.wire_values().into_iter().cloned().collect();
433
434 let rebuilt =
435 PreparedModelInput::from_identity_wire_values(identity, values, describe).unwrap();
436 assert_eq!(rebuilt, input);
437 assert_eq!(graph.execution_order(), [0, 1]);
438 assert_eq!(graph.output(), 1);
439 assert_eq!(rebuilt.wire_values()[2].marker, 3);
440 assert_eq!(
441 rebuilt.parts()[1].extents(),
442 &[InputExtent::PatchGrid {
443 time: 1,
444 height: 2,
445 width: 2,
446 }]
447 );
448 }
449
450 #[test]
451 fn rejects_payload_geometry_that_disagrees_with_wire_identity() {
452 let input = PreparedModelInput::new(
453 vec![PreparedInputPart::new(
454 InputModality::Text,
455 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
456 [],
457 )
458 .unwrap()],
459 describe,
460 )
461 .unwrap();
462 let wrong = vec![fake(TensorDtype::U32, &[1, 3], 1)];
463
464 assert!(matches!(
465 PreparedModelInput::from_identity_wire_values(
466 input.identity().clone(),
467 wrong,
468 describe
469 ),
470 Err(PreparedInputError::WireIdentityMismatch)
471 ));
472 }
473
474 #[test]
475 fn rejects_incompatible_payload_at_part_construction() {
476 let result = PreparedInputPart::new(
477 InputModality::Text,
478 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 2], 1)),
479 [],
480 );
481
482 assert!(matches!(
483 result,
484 Err(PreparedInputError::IncompatiblePayload {
485 modality: InputModality::Text,
486 payload: InputPayloadKind::Tensor,
487 })
488 ));
489 }
490
491 #[test]
492 fn rejects_incompatible_metadata_at_part_construction() {
493 let result = PreparedInputPart::new(
494 InputModality::Text,
495 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
496 [(
497 InputMetadataKey::PatchGrid,
498 fake(TensorDtype::I32, &[1, 3], 2),
499 )],
500 );
501
502 assert!(matches!(
503 result,
504 Err(PreparedInputError::IncompatibleMetadata {
505 modality: InputModality::Text,
506 key: InputMetadataKey::PatchGrid,
507 })
508 ));
509 }
510
511 #[test]
512 fn prompt_cache_identity_requires_both_prepared_description_and_semantic_content() {
513 let first = PreparedModelInput::new(
514 vec![PreparedInputPart::new(
515 InputModality::Image,
516 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 3, 4, 4], 1)),
517 [],
518 )
519 .unwrap()],
520 describe,
521 )
522 .unwrap();
523 let reshaped = PreparedModelInput::new(
524 vec![PreparedInputPart::new(
525 InputModality::Image,
526 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 3, 8, 8], 2)),
527 [],
528 )
529 .unwrap()],
530 describe,
531 )
532 .unwrap();
533
534 let image_a = first.cache_identity("sha256:image-a").unwrap();
535 let image_b = first.cache_identity("sha256:image-b").unwrap();
536 let reshaped_a = reshaped.cache_identity("sha256:image-a").unwrap();
537
538 assert_ne!(
539 image_a.prefix_content_fingerprint(),
540 image_b.prefix_content_fingerprint()
541 );
542 assert_ne!(
543 image_a.prefix_content_fingerprint(),
544 reshaped_a.prefix_content_fingerprint()
545 );
546 assert_eq!(image_a.prepared(), first.identity());
547 assert!(first.cache_identity(" ").is_err());
548 }
549}