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
233#[derive(Debug, thiserror::Error)]
235pub enum PreparedInputCacheIdentityError {
236 #[error("prepared-input semantic content fingerprint must not be empty")]
238 EmptySemanticContent,
239 #[error("prepared-input cache identity is invalid: {0}")]
241 Prepared(PreparedInputError),
242}
243
244impl<Tensor> PreparedModelInput<Tensor> {
245 pub fn new(
247 parts: Vec<PreparedInputPart<Tensor>>,
248 describe: impl Fn(&Tensor) -> Result<InputTensorIdentity, PreparedInputError>,
249 ) -> Result<Self, PreparedInputError> {
250 let identity = PreparedInputIdentity::new(
251 parts
252 .iter()
253 .map(|part| part.descriptor(&describe))
254 .collect::<Result<Vec<_>, _>>()?,
255 )?;
256 Ok(Self { parts, identity })
257 }
258
259 pub const fn identity(&self) -> &PreparedInputIdentity {
261 &self.identity
262 }
263
264 pub fn cache_identity(
266 &self,
267 semantic_content_fingerprint: impl Into<String>,
268 ) -> Result<PreparedInputCacheIdentity, PreparedInputCacheIdentityError> {
269 PreparedInputCacheIdentity::new(self.identity.clone(), semantic_content_fingerprint)
270 }
271
272 pub fn parts(&self) -> &[PreparedInputPart<Tensor>] {
274 &self.parts
275 }
276
277 pub fn len(&self) -> usize {
279 self.parts.len()
280 }
281
282 pub fn is_empty(&self) -> bool {
284 self.parts.is_empty()
285 }
286
287 pub fn wire_values(&self) -> Vec<&Tensor> {
289 let mut values = Vec::new();
290 for part in &self.parts {
291 values.push(part.payload.value());
292 values.extend(part.metadata.values());
293 }
294 values
295 }
296
297 pub fn from_identity_wire_values(
299 identity: PreparedInputIdentity,
300 values: Vec<Tensor>,
301 describe: impl Fn(&Tensor) -> Result<InputTensorIdentity, PreparedInputError>,
302 ) -> Result<Self, PreparedInputError> {
303 let expected_values = identity
304 .parts()
305 .iter()
306 .map(|part| 1 + part.metadata().len())
307 .sum::<usize>();
308 if values.len() != expected_values {
309 return Err(PreparedInputError::WireValueCount {
310 expected: expected_values,
311 actual: values.len(),
312 });
313 }
314 let mut values = values.into_iter();
315 let mut parts = Vec::with_capacity(identity.len());
316 for descriptor in identity.parts() {
317 let payload = values.next().expect("validated prepared-input value count");
318 let payload = match descriptor.payload_kind() {
319 InputPayloadKind::TokenIds => PreparedInputPayload::TokenIds(payload),
320 InputPayloadKind::Tensor => PreparedInputPayload::Tensor(payload),
321 InputPayloadKind::Embeddings => PreparedInputPayload::Embeddings(payload),
322 payload_kind => {
323 return Err(PreparedInputError::IncompatiblePayload {
324 modality: descriptor.modality(),
325 payload: payload_kind,
326 });
327 }
328 };
329 let metadata = descriptor
330 .metadata()
331 .keys()
332 .copied()
333 .map(|key| {
334 (
335 key,
336 values.next().expect("validated prepared-input value count"),
337 )
338 })
339 .collect::<Vec<_>>();
340 parts.push(PreparedInputPart::new_with_extents(
341 descriptor.modality(),
342 payload,
343 metadata,
344 descriptor.extents(),
345 )?);
346 }
347 let actual = Self::new(parts, describe)?;
348 if actual.identity != identity {
349 return Err(PreparedInputError::WireIdentityMismatch);
350 }
351 Ok(actual)
352 }
353
354 pub fn into_parts(self) -> Vec<PreparedInputPart<Tensor>> {
356 self.parts
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use eredu_core::{checkpoint::TensorDtype, PreparedInputError};
363
364 use super::*;
365
366 #[derive(Debug, Clone, Eq, PartialEq)]
367 struct FakeTensor {
368 dtype: TensorDtype,
369 shape: Vec<usize>,
370 marker: u8,
371 }
372
373 fn fake(dtype: TensorDtype, shape: &[usize], marker: u8) -> FakeTensor {
374 FakeTensor {
375 dtype,
376 shape: shape.to_vec(),
377 marker,
378 }
379 }
380
381 fn describe(value: &FakeTensor) -> Result<InputTensorIdentity, PreparedInputError> {
382 InputTensorIdentity::new(value.dtype.clone(), value.shape.clone())
383 }
384
385 #[test]
386 fn composite_input_extension_binds_typed_parts_to_a_multi_group_graph() {
387 let graph = crate::ExecutionGraph::new(
388 vec![
389 crate::ExecutionGroupSpec::root("vision"),
390 crate::ExecutionGroupSpec::with_dependencies("text", ["vision"]),
391 ],
392 "text",
393 )
394 .unwrap();
395 let input = PreparedModelInput::new(
396 vec![
397 PreparedInputPart::new(
398 InputModality::Text,
399 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
400 [],
401 )
402 .unwrap(),
403 PreparedInputPart::new_with_extents(
404 InputModality::Image,
405 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[4, 12], 2)),
406 [(
407 InputMetadataKey::PatchGrid,
408 fake(TensorDtype::I32, &[1, 3], 3),
409 )],
410 [InputExtent::PatchGrid {
411 time: 1,
412 height: 2,
413 width: 2,
414 }],
415 )
416 .unwrap(),
417 ],
418 describe,
419 )
420 .unwrap();
421 let identity = input.identity().clone();
422 let values = input.wire_values().into_iter().cloned().collect();
423
424 let rebuilt =
425 PreparedModelInput::from_identity_wire_values(identity, values, describe).unwrap();
426 assert_eq!(rebuilt, input);
427 assert_eq!(graph.execution_order(), [0, 1]);
428 assert_eq!(graph.output(), 1);
429 assert_eq!(rebuilt.wire_values()[2].marker, 3);
430 assert_eq!(
431 rebuilt.parts()[1].extents(),
432 &[InputExtent::PatchGrid {
433 time: 1,
434 height: 2,
435 width: 2,
436 }]
437 );
438 }
439
440 #[test]
441 fn rejects_payload_geometry_that_disagrees_with_wire_identity() {
442 let input = PreparedModelInput::new(
443 vec![PreparedInputPart::new(
444 InputModality::Text,
445 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
446 [],
447 )
448 .unwrap()],
449 describe,
450 )
451 .unwrap();
452 let wrong = vec![fake(TensorDtype::U32, &[1, 3], 1)];
453
454 assert!(matches!(
455 PreparedModelInput::from_identity_wire_values(
456 input.identity().clone(),
457 wrong,
458 describe
459 ),
460 Err(PreparedInputError::WireIdentityMismatch)
461 ));
462 }
463
464 #[test]
465 fn rejects_incompatible_payload_at_part_construction() {
466 let result = PreparedInputPart::new(
467 InputModality::Text,
468 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 2], 1)),
469 [],
470 );
471
472 assert!(matches!(
473 result,
474 Err(PreparedInputError::IncompatiblePayload {
475 modality: InputModality::Text,
476 payload: InputPayloadKind::Tensor,
477 })
478 ));
479 }
480
481 #[test]
482 fn rejects_incompatible_metadata_at_part_construction() {
483 let result = PreparedInputPart::new(
484 InputModality::Text,
485 PreparedInputPayload::TokenIds(fake(TensorDtype::U32, &[1, 2], 1)),
486 [(
487 InputMetadataKey::PatchGrid,
488 fake(TensorDtype::I32, &[1, 3], 2),
489 )],
490 );
491
492 assert!(matches!(
493 result,
494 Err(PreparedInputError::IncompatibleMetadata {
495 modality: InputModality::Text,
496 key: InputMetadataKey::PatchGrid,
497 })
498 ));
499 }
500
501 #[test]
502 fn prompt_cache_identity_requires_both_prepared_description_and_semantic_content() {
503 let first = PreparedModelInput::new(
504 vec![PreparedInputPart::new(
505 InputModality::Image,
506 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 3, 4, 4], 1)),
507 [],
508 )
509 .unwrap()],
510 describe,
511 )
512 .unwrap();
513 let reshaped = PreparedModelInput::new(
514 vec![PreparedInputPart::new(
515 InputModality::Image,
516 PreparedInputPayload::Tensor(fake(TensorDtype::F32, &[1, 3, 8, 8], 2)),
517 [],
518 )
519 .unwrap()],
520 describe,
521 )
522 .unwrap();
523
524 let image_a = first.cache_identity("sha256:image-a").unwrap();
525 let image_b = first.cache_identity("sha256:image-b").unwrap();
526 let reshaped_a = reshaped.cache_identity("sha256:image-a").unwrap();
527
528 assert_ne!(
529 image_a.prefix_content_fingerprint(),
530 image_b.prefix_content_fingerprint()
531 );
532 assert_ne!(
533 image_a.prefix_content_fingerprint(),
534 reshaped_a.prefix_content_fingerprint()
535 );
536 assert_eq!(image_a.prepared(), first.identity());
537 assert!(first.cache_identity(" ").is_err());
538 }
539}