1use std::collections::{BTreeMap, HashMap, HashSet};
30
31use serde_json::{Map, Value};
32
33use super::LayoutError;
34
35pub const MAX_CONTRACT_WIRE_BYTES: usize = 256 << 10;
37
38fn dtype_bytes(dtype: &str) -> Option<i64> {
40 Some(match dtype {
41 "uint8" | "int8" | "float8_e4m3fn" | "float8_e5m2" | "float8_e8m0fnu" => 1,
42 "bfloat16" | "float16" => 2,
43 "int32" | "float32" => 4,
44 "int64" | "float64" => 8,
45 _ => return None,
46 })
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct GroupLayout {
52 pub group_id: String,
54 pub cache_blocks_per_lcm_block: i64,
56 pub page_count: i64,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PlaneLayout {
63 pub plane_id: String,
65 pub bytes_per_lcm_block: i64,
67 pub arena_offset_bytes: i64,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct FieldLayout {
74 pub group_id: String,
76 pub field_id: String,
78 pub plane_id: String,
80 pub shape: Vec<i64>,
82 pub dtype: String,
84 pub field_offset_bytes: i64,
86 pub page_stride_bytes: i64,
88}
89
90impl FieldLayout {
91 pub fn element_size(&self) -> i64 {
93 dtype_bytes(&self.dtype).expect("checked when parsed")
94 }
95
96 pub fn payload_bytes(&self) -> i64 {
98 self.shape.iter().product::<i64>() * self.element_size()
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct GroupSpec {
105 pub group_id: String,
107 pub attn_kind: String,
109 pub rows_per_page: Option<i64>,
111 pub entry_stride_tokens: Option<i64>,
113 pub sliding_window_tokens: Option<i64>,
115 pub replayable: bool,
117 pub transfer_policy: Option<String>,
119 pub shard_count: i64,
121}
122
123impl GroupSpec {
124 pub fn block_granularity(&self) -> Result<i64, LayoutError> {
127 match (self.rows_per_page, self.entry_stride_tokens) {
128 (Some(rows), Some(stride)) => Ok(rows * stride),
129 _ => Err(LayoutError::new(format!(
130 "group {:?}: block_granularity is undefined for a state group",
131 self.group_id
132 ))),
133 }
134 }
135
136 pub fn full_suffix(&self) -> bool {
139 self.transfer_policy.as_deref() == Some("full_suffix")
140 }
141
142 fn validate(&self) -> Result<(), LayoutError> {
143 let id = &self.group_id;
144 if self.shard_count <= 0 {
145 return Err(LayoutError::new("shard_count must be a positive integer"));
146 }
147 if !matches!(
148 self.attn_kind.as_str(),
149 "full_history" | "sliding_window" | "state"
150 ) {
151 return Err(LayoutError::new(format!(
152 "group {id:?}: unknown cache-group kind {:?}",
153 self.attn_kind
154 )));
155 }
156 if self.attn_kind == "sliding_window" {
157 if self.sliding_window_tokens.is_none_or(|tokens| tokens <= 0) {
158 return Err(LayoutError::new(format!(
159 "group {id:?}: a sliding-window group needs a positive sliding_window_tokens"
160 )));
161 }
162 } else if self.sliding_window_tokens.is_some() {
163 return Err(LayoutError::new(format!(
164 "group {id:?}: only a sliding-window group carries sliding_window_tokens"
165 )));
166 }
167 if self.replayable && self.attn_kind != "sliding_window" {
168 return Err(LayoutError::new(format!(
169 "group {id:?}: only a sliding-window group can be regenerated by bounded replay"
170 )));
171 }
172 let has_rows = self.rows_per_page.is_some() || self.entry_stride_tokens.is_some();
173 if self.attn_kind == "state" {
174 if has_rows {
175 return Err(LayoutError::new(format!(
176 "group {id:?}: a state group declares no row geometry"
177 )));
178 }
179 return Ok(());
180 }
181 match (self.rows_per_page, self.entry_stride_tokens) {
182 (Some(rows), Some(stride)) if rows > 0 && stride > 0 => Ok(()),
183 (Some(_), Some(_)) => Err(LayoutError::new(format!(
184 "group {id:?}: rows_per_page and entry_stride_tokens must be > 0"
185 ))),
186 _ => Err(LayoutError::new(format!(
187 "group {id:?}: a {} group declares row geometry",
188 self.attn_kind
189 ))),
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct Partition {
198 pub axis: i64,
200 pub global_extent: i64,
202 pub global_parts: Vec<i64>,
204}
205
206impl Partition {
207 fn validate(&self, field_id: &str, local_shape: &[i64]) -> Result<(), LayoutError> {
208 let invalid = || {
209 LayoutError::new(format!(
210 "cache field {field_id:?} has invalid partition geometry"
211 ))
212 };
213 let axis = usize::try_from(self.axis).map_err(|_| invalid())?;
214 let local_extent = *local_shape.get(axis).ok_or_else(invalid)?;
215 if self.global_extent < local_extent || local_extent <= 0 {
216 return Err(invalid());
217 }
218 if self.global_extent % local_extent != 0 {
219 return Err(LayoutError::new(format!(
220 "cache field {field_id:?} global partition extent is not divisible by its \
221 local extent"
222 )));
223 }
224 let distinct_shards = self.global_extent / local_extent;
225 if !self.global_parts.is_empty()
226 && (self.global_parts.len() < 2
227 || self.global_parts.len() > 16
228 || self
229 .global_parts
230 .iter()
231 .any(|&part| part <= 0 || part % distinct_shards != 0)
232 || self.global_parts.iter().sum::<i64>() != self.global_extent)
233 {
234 return Err(LayoutError::new(format!(
235 "cache field {field_id:?} has invalid global partition parts"
236 )));
237 }
238 Ok(())
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct Plan {
245 pub prefix_granularity: i64,
247 pub lcm_block_bytes: i64,
249 pub num_lcm_blocks: i64,
251 pub groups: Vec<GroupLayout>,
253 pub planes: Vec<PlaneLayout>,
255 pub fields: Vec<FieldLayout>,
257 pub resident_block_bytes: Option<i64>,
260}
261
262#[derive(Debug, Clone)]
264pub struct Contract {
265 pub plan: Plan,
267 pub group_specs: Vec<GroupSpec>,
269 partitions: HashMap<String, Partition>,
271 pub field_addresses: Option<BTreeMap<String, u64>>,
274 groups_by_id: HashMap<String, usize>,
275 specs_by_id: HashMap<String, usize>,
276 fields_by_id: HashMap<String, usize>,
277 planes_by_id: HashMap<String, usize>,
278 group_fields: HashMap<String, Vec<usize>>,
280}
281
282impl PartialEq for Contract {
283 fn eq(&self, other: &Self) -> bool {
284 self.plan == other.plan
285 && self.group_specs == other.group_specs
286 && self.partitions == other.partitions
287 && self.field_addresses == other.field_addresses
288 }
289}
290
291impl Eq for Contract {}
292
293fn keyword_fields<'a>(
296 value: &'a Value,
297 name: &str,
298 required: &[&str],
299 optional: &[&str],
300) -> Result<&'a Map<String, Value>, LayoutError> {
301 let object = value
302 .as_object()
303 .ok_or_else(|| LayoutError::new(format!("{name} must be a JSON object")))?;
304 if let Some(unknown) = object
305 .keys()
306 .find(|key| !required.contains(&key.as_str()) && !optional.contains(&key.as_str()))
307 {
308 return Err(LayoutError::new(format!(
309 "{name} has an unknown field {unknown:?}"
310 )));
311 }
312 if let Some(missing) = required.iter().find(|key| !object.contains_key(**key)) {
313 return Err(LayoutError::new(format!("{name} misses {missing:?}")));
314 }
315 Ok(object)
316}
317
318fn key<'a>(
319 object: &'a Map<String, Value>,
320 name: &str,
321 key: &str,
322) -> Result<&'a Value, LayoutError> {
323 object
324 .get(key)
325 .ok_or_else(|| LayoutError::new(format!("{name} misses {key:?}")))
326}
327
328fn int(value: &Value, what: &str) -> Result<i64, LayoutError> {
329 value
330 .as_i64()
331 .ok_or_else(|| LayoutError::new(format!("{what} must be an integer")))
332}
333
334fn opt_int(object: &Map<String, Value>, field: &str) -> Result<Option<i64>, LayoutError> {
335 match object.get(field) {
336 None | Some(Value::Null) => Ok(None),
337 Some(value) => int(value, field).map(Some),
338 }
339}
340
341fn string(value: &Value, what: &str) -> Result<String, LayoutError> {
342 value
343 .as_str()
344 .map(str::to_owned)
345 .ok_or_else(|| LayoutError::new(format!("{what} must be a string")))
346}
347
348fn list<'a>(value: &'a Value, what: &str) -> Result<&'a Vec<Value>, LayoutError> {
349 value
350 .as_array()
351 .ok_or_else(|| LayoutError::new(format!("{what} must be a list")))
352}
353
354pub(crate) fn load_wire_json(
356 raw: &[u8],
357 name: &str,
358 maximum: usize,
359) -> Result<Map<String, Value>, LayoutError> {
360 if raw.is_empty() || raw.len() > maximum {
361 return Err(LayoutError::new(format!(
362 "{name} payload must be 1..{maximum} bytes"
363 )));
364 }
365 match serde_json::from_slice::<Value>(raw) {
366 Ok(Value::Object(object)) => Ok(object),
367 Ok(_) => Err(LayoutError::new(format!(
368 "{name} payload must be a JSON object"
369 ))),
370 Err(_) => Err(LayoutError::new(format!("invalid {name} JSON"))),
371 }
372}
373
374fn parse_group(value: &Value) -> Result<GroupLayout, LayoutError> {
375 let name = "cache group layout";
376 let object = keyword_fields(
377 value,
378 name,
379 &["group_id", "cache_blocks_per_lcm_block", "page_count"],
380 &[],
381 )?;
382 Ok(GroupLayout {
383 group_id: string(key(object, name, "group_id")?, "group_id")?,
384 cache_blocks_per_lcm_block: int(
385 key(object, name, "cache_blocks_per_lcm_block")?,
386 "cache_blocks_per_lcm_block",
387 )?,
388 page_count: int(key(object, name, "page_count")?, "page_count")?,
389 })
390}
391
392fn parse_plane(value: &Value) -> Result<PlaneLayout, LayoutError> {
393 let name = "cache plane layout";
394 let object = keyword_fields(
395 value,
396 name,
397 &["plane_id", "bytes_per_lcm_block", "arena_offset_bytes"],
398 &[],
399 )?;
400 Ok(PlaneLayout {
401 plane_id: string(key(object, name, "plane_id")?, "plane_id")?,
402 bytes_per_lcm_block: int(
403 key(object, name, "bytes_per_lcm_block")?,
404 "bytes_per_lcm_block",
405 )?,
406 arena_offset_bytes: int(
407 key(object, name, "arena_offset_bytes")?,
408 "arena_offset_bytes",
409 )?,
410 })
411}
412
413fn parse_field(value: &Value) -> Result<FieldLayout, LayoutError> {
414 let name = "cache field layout";
415 let object = keyword_fields(
416 value,
417 name,
418 &[
419 "group_id",
420 "field_id",
421 "plane_id",
422 "shape",
423 "dtype",
424 "field_offset_bytes",
425 "page_stride_bytes",
426 ],
427 &[],
428 )?;
429 let field = FieldLayout {
430 group_id: string(key(object, name, "group_id")?, "group_id")?,
431 field_id: string(key(object, name, "field_id")?, "field_id")?,
432 plane_id: string(key(object, name, "plane_id")?, "plane_id")?,
433 shape: list(key(object, name, "shape")?, "shape")?
434 .iter()
435 .map(|dimension| int(dimension, "shape"))
436 .collect::<Result<_, _>>()?,
437 dtype: string(key(object, name, "dtype")?, "dtype")?,
438 field_offset_bytes: int(
439 key(object, name, "field_offset_bytes")?,
440 "field_offset_bytes",
441 )?,
442 page_stride_bytes: int(key(object, name, "page_stride_bytes")?, "page_stride_bytes")?,
443 };
444 if dtype_bytes(&field.dtype).is_none() {
445 return Err(LayoutError::new(format!(
446 "unsupported cache dtype {:?}",
447 field.dtype
448 )));
449 }
450 Ok(field)
451}
452
453fn parse_spec(value: &Value) -> Result<GroupSpec, LayoutError> {
454 let name = "cache group spec";
455 let object = keyword_fields(
456 value,
457 name,
458 &["group_id", "attn_kind", "replayable"],
459 &[
460 "rows_per_page",
461 "entry_stride_tokens",
462 "sliding_window_tokens",
463 "transfer_policy",
464 "shard_count",
465 ],
466 )?;
467 let spec = GroupSpec {
468 group_id: string(key(object, name, "group_id")?, "group_id")?,
469 attn_kind: string(key(object, name, "attn_kind")?, "attn_kind")?,
470 rows_per_page: opt_int(object, "rows_per_page")?,
471 entry_stride_tokens: opt_int(object, "entry_stride_tokens")?,
472 sliding_window_tokens: opt_int(object, "sliding_window_tokens")?,
473 replayable: key(object, name, "replayable")?
474 .as_bool()
475 .ok_or_else(|| LayoutError::new("replayable must be a bool"))?,
476 transfer_policy: match object.get("transfer_policy") {
477 None | Some(Value::Null) => None,
478 Some(value) => Some(string(value, "transfer_policy")?),
479 },
480 shard_count: match object.get("shard_count") {
481 None => 1,
482 Some(value) => int(value, "shard_count")?,
483 },
484 };
485 spec.validate()?;
486 Ok(spec)
487}
488
489fn parse_partition(value: &Value) -> Result<Partition, LayoutError> {
490 let name = "cache field partition";
491 let object = value
492 .as_object()
493 .ok_or_else(|| LayoutError::new(format!("{name} must be a JSON object")))?;
494 Ok(Partition {
495 axis: int(key(object, name, "axis")?, "axis")?,
496 global_extent: int(key(object, name, "global_extent")?, "global_extent")?,
497 global_parts: list(key(object, name, "global_parts")?, "global_parts")?
498 .iter()
499 .map(|part| int(part, "global_parts"))
500 .collect::<Result<_, _>>()?,
501 })
502}
503
504impl Contract {
505 pub fn from_wire(raw: &[u8]) -> Result<Self, LayoutError> {
507 let name = "cache transfer contract";
508 let payload = load_wire_json(raw, name, MAX_CONTRACT_WIRE_BYTES)?;
509 let invalid = |error: LayoutError| {
510 LayoutError::new(format!("invalid cache transfer contract: {error}"))
511 };
512 let parse = || -> Result<Self, LayoutError> {
513 let plan = key(&payload, name, "plan")?
514 .as_object()
515 .ok_or_else(|| LayoutError::new("plan must be a JSON object"))?;
516 let plan = Plan {
517 prefix_granularity: int(
518 key(plan, "plan", "prefix_granularity")?,
519 "prefix_granularity",
520 )?,
521 lcm_block_bytes: int(key(plan, "plan", "lcm_block_bytes")?, "lcm_block_bytes")?,
522 num_lcm_blocks: int(key(plan, "plan", "num_lcm_blocks")?, "num_lcm_blocks")?,
523 groups: list(key(plan, "plan", "groups")?, "groups")?
524 .iter()
525 .map(parse_group)
526 .collect::<Result<_, _>>()?,
527 planes: list(key(plan, "plan", "planes")?, "planes")?
528 .iter()
529 .map(parse_plane)
530 .collect::<Result<_, _>>()?,
531 fields: list(key(plan, "plan", "fields")?, "fields")?
532 .iter()
533 .map(parse_field)
534 .collect::<Result<_, _>>()?,
535 resident_block_bytes: opt_int(plan, "resident_block_bytes")?,
536 };
537 let field_addresses = match payload.get("field_addresses") {
538 None | Some(Value::Null) => None,
539 Some(Value::Object(addresses)) => Some(
540 addresses
541 .iter()
542 .map(|(field, address)| {
543 let address =
544 address.as_u64().filter(|&address| address > 0).ok_or_else(
545 || LayoutError::new("field addresses must be nonzero uint64"),
546 )?;
547 Ok((field.clone(), address))
548 })
549 .collect::<Result<_, LayoutError>>()?,
550 ),
551 Some(_) => return Err(LayoutError::new("field addresses must be a mapping")),
552 };
553 let group_specs = list(key(&payload, name, "group_specs")?, "group_specs")?
554 .iter()
555 .map(parse_spec)
556 .collect::<Result<Vec<_>, _>>()?;
557 let schema = key(&payload, name, "transfer_schema")?
558 .as_object()
559 .ok_or_else(|| LayoutError::new("transfer_schema must be a JSON object"))?;
560 let mut partitions = Vec::new();
561 for entry in list(key(schema, "transfer_schema", "fields")?, "fields")? {
562 let entry = entry
563 .as_object()
564 .ok_or_else(|| LayoutError::new("a transfer field must be a JSON object"))?;
565 let field_id = string(key(entry, "transfer field", "field_id")?, "field_id")?;
566 if field_id.is_empty() {
567 return Err(LayoutError::new(
568 "cache transfer field_id must be non-empty",
569 ));
570 }
571 partitions.push((
572 field_id,
573 parse_partition(key(entry, "transfer field", "partition")?)?,
574 ));
575 }
576 Self::new(plan, group_specs, partitions, field_addresses)
577 };
578 parse().map_err(invalid)
579 }
580
581 pub fn new(
584 plan: Plan,
585 group_specs: Vec<GroupSpec>,
586 partitions: Vec<(String, Partition)>,
587 field_addresses: Option<BTreeMap<String, u64>>,
588 ) -> Result<Self, LayoutError> {
589 let plan_groups: HashSet<&str> = plan.groups.iter().map(|g| g.group_id.as_str()).collect();
590 let spec_groups: HashSet<&str> = group_specs.iter().map(|s| s.group_id.as_str()).collect();
591 if plan_groups != spec_groups {
592 return Err(LayoutError::new(
593 "cache plan and scheduler group IDs disagree",
594 ));
595 }
596 let mut seen = HashSet::new();
597 if partitions
598 .iter()
599 .any(|(field, _)| !seen.insert(field.clone()))
600 {
601 return Err(LayoutError::new(
602 "cache transfer schema contains a duplicate field",
603 ));
604 }
605 let fields_by_id: HashMap<String, usize> = plan
607 .fields
608 .iter()
609 .enumerate()
610 .map(|(index, field)| (field.field_id.clone(), index))
611 .collect();
612 for (field_id, partition) in &partitions {
613 let Some(&index) = fields_by_id.get(field_id) else {
614 return Err(LayoutError::new(format!(
615 "cache transfer schema references unknown fields [{field_id:?}]"
616 )));
617 };
618 partition.validate(field_id, &plan.fields[index].shape)?;
619 }
620 if let Some(addresses) = &field_addresses {
621 let planned: HashSet<&str> = plan.fields.iter().map(|f| f.field_id.as_str()).collect();
622 let addressed: HashSet<&str> = addresses.keys().map(String::as_str).collect();
623 if planned != addressed {
624 return Err(LayoutError::new(
625 "field addresses must cover the complete plan",
626 ));
627 }
628 }
629 let groups_by_id = plan
630 .groups
631 .iter()
632 .enumerate()
633 .map(|(index, group)| (group.group_id.clone(), index))
634 .collect();
635 let specs_by_id = group_specs
636 .iter()
637 .enumerate()
638 .map(|(index, spec)| (spec.group_id.clone(), index))
639 .collect();
640 let planes_by_id = plan
641 .planes
642 .iter()
643 .enumerate()
644 .map(|(index, plane)| (plane.plane_id.clone(), index))
645 .collect();
646 let mut group_fields: HashMap<String, Vec<usize>> = HashMap::new();
647 for (index, field) in plan.fields.iter().enumerate() {
648 group_fields
649 .entry(field.group_id.clone())
650 .or_default()
651 .push(index);
652 }
653 for indexes in group_fields.values_mut() {
654 indexes.sort_by(|&a, &b| plan.fields[a].field_id.cmp(&plan.fields[b].field_id));
655 }
656 Ok(Self {
657 plan,
658 group_specs,
659 partitions: partitions.into_iter().collect(),
660 field_addresses,
661 groups_by_id,
662 specs_by_id,
663 fields_by_id,
664 planes_by_id,
665 group_fields,
666 })
667 }
668
669 pub fn without_addresses(&self) -> Self {
672 Self {
673 field_addresses: None,
674 ..self.clone()
675 }
676 }
677
678 pub fn group_spec(&self, group_id: &str) -> Result<&GroupSpec, LayoutError> {
680 self.specs_by_id
681 .get(group_id)
682 .map(|&index| &self.group_specs[index])
683 .ok_or_else(|| LayoutError::new(format!("unknown cache group {group_id:?}")))
684 }
685
686 pub fn group(&self, group_id: &str) -> Result<&GroupLayout, LayoutError> {
688 self.groups_by_id
689 .get(group_id)
690 .map(|&index| &self.plan.groups[index])
691 .ok_or_else(|| LayoutError::new(format!("unknown cache group {group_id:?}")))
692 }
693
694 pub fn field(&self, field_id: &str) -> Result<&FieldLayout, LayoutError> {
696 self.fields_by_id
697 .get(field_id)
698 .map(|&index| &self.plan.fields[index])
699 .ok_or_else(|| LayoutError::new(format!("unknown cache field {field_id:?}")))
700 }
701
702 pub fn fields_for_group(&self, group_id: &str) -> Vec<&FieldLayout> {
704 self.group_fields
705 .get(group_id)
706 .map(|indexes| {
707 indexes
708 .iter()
709 .map(|&index| &self.plan.fields[index])
710 .collect()
711 })
712 .unwrap_or_default()
713 }
714
715 pub fn shard_count(&self, group_id: &str) -> Result<i64, LayoutError> {
717 Ok(self.group_spec(group_id)?.shard_count)
718 }
719
720 pub fn virtual_block_count(&self, group_id: &str) -> Result<i64, LayoutError> {
723 let pages = self.group(group_id)?.page_count;
724 Ok(1 + (pages - 1) * self.shard_count(group_id)?)
725 }
726
727 pub fn partition_for(&self, field_id: &str) -> Option<&Partition> {
729 self.partitions.get(field_id)
730 }
731
732 pub fn field_page_byte_offset(&self, field_id: &str, page_id: i64) -> Result<i64, LayoutError> {
734 let field = self.field(field_id)?;
735 let group = self.group(&field.group_id)?;
736 if page_id < 0 || page_id >= group.page_count {
737 return Err(LayoutError::new(format!(
738 "page_id {page_id} outside [0, {}) for group {:?}",
739 group.page_count, group.group_id
740 )));
741 }
742 let plane = self
743 .planes_by_id
744 .get(&field.plane_id)
745 .map(|&index| &self.plan.planes[index])
746 .ok_or_else(|| LayoutError::new(format!("unknown cache plane {:?}", field.plane_id)))?;
747 Ok(
748 plane.arena_offset_bytes + plane.bytes_per_lcm_block - field.page_stride_bytes
749 + page_id * field.page_stride_bytes
750 + field.field_offset_bytes,
751 )
752 }
753
754 pub fn field_address(&self, base: u64, field_id: &str) -> Result<i128, LayoutError> {
757 if let Some(addresses) = &self.field_addresses {
758 return addresses
759 .get(field_id)
760 .map(|&address| i128::from(address))
761 .ok_or_else(|| LayoutError::new(format!("unknown cache field {field_id:?}")));
762 }
763 Ok(i128::from(base) + i128::from(self.field_page_byte_offset(field_id, 0)?))
764 }
765}
766
767fn global_shape(field: &FieldLayout, partition: Option<&Partition>) -> Vec<i64> {
770 let mut shape = field.shape.clone();
771 if let Some(partition) = partition {
772 if let Some(extent) = usize::try_from(partition.axis)
773 .ok()
774 .and_then(|axis| shape.get_mut(axis))
775 {
776 *extent = partition.global_extent;
777 }
778 }
779 shape
780}
781
782pub fn validate_peer(layout: &Contract, peer: &Contract) -> Result<(), LayoutError> {
788 if layout.plan.prefix_granularity != peer.plan.prefix_granularity {
789 return Err(LayoutError::new(
790 "Cache P/D contract mismatch: prefix_granularity",
791 ));
792 }
793 let local_ids: Vec<&str> = layout
794 .group_specs
795 .iter()
796 .map(|s| s.group_id.as_str())
797 .collect();
798 let peer_ids: Vec<&str> = peer
799 .group_specs
800 .iter()
801 .map(|s| s.group_id.as_str())
802 .collect();
803 if local_ids != peer_ids {
804 return Err(LayoutError::new("Cache P/D contract mismatch: group order"));
805 }
806 for (local, remote) in layout.group_specs.iter().zip(&peer.group_specs) {
807 if local.attn_kind != remote.attn_kind
808 || local.rows_per_page != remote.rows_per_page
809 || local.entry_stride_tokens != remote.entry_stride_tokens
810 || local.sliding_window_tokens != remote.sliding_window_tokens
811 || local.replayable != remote.replayable
812 || local.transfer_policy != remote.transfer_policy
813 {
814 return Err(LayoutError::new(format!(
815 "Cache P/D contract mismatch: group {:?} semantics",
816 local.group_id
817 )));
818 }
819 let local_fields = layout.fields_for_group(&local.group_id);
820 let peer_fields = peer.fields_for_group(&remote.group_id);
821 let local_field_ids: Vec<&str> = local_fields.iter().map(|f| f.field_id.as_str()).collect();
822 let peer_field_ids: Vec<&str> = peer_fields.iter().map(|f| f.field_id.as_str()).collect();
823 if local_field_ids != peer_field_ids {
824 return Err(LayoutError::new(format!(
825 "Cache P/D contract mismatch: group {:?} transfer field order",
826 local.group_id
827 )));
828 }
829 for (local_field, peer_field) in local_fields.iter().zip(&peer_fields) {
830 let local_partition = layout.partition_for(&local_field.field_id);
831 let peer_partition = peer.partition_for(&peer_field.field_id);
832 if local_field.dtype != peer_field.dtype
833 || local_partition != peer_partition
834 || global_shape(local_field, local_partition)
835 != global_shape(peer_field, peer_partition)
836 {
837 return Err(LayoutError::new(format!(
838 "Cache P/D contract mismatch: field {:?} semantics",
839 local_field.field_id
840 )));
841 }
842 }
843 }
844 Ok(())
845}
846
847#[cfg(test)]
848pub(crate) mod tests {
849 use super::*;
850
851 pub(crate) fn wire() -> String {
852 serde_json::json!({
853 "plan": {
854 "prefix_granularity": 2,
855 "lcm_block_bytes": 32,
856 "num_lcm_blocks": 15,
857 "groups": [
858 {"group_id": "history", "cache_blocks_per_lcm_block": 1, "page_count": 16},
859 {"group_id": "state", "cache_blocks_per_lcm_block": 1, "page_count": 16}
860 ],
861 "planes": [
862 {"plane_id": "p0", "bytes_per_lcm_block": 32, "arena_offset_bytes": 0},
863 {"plane_id": "p1", "bytes_per_lcm_block": 32, "arena_offset_bytes": 16}
864 ],
865 "fields": [
866 {"group_id": "history", "field_id": "layer.0.kv", "plane_id": "p0",
867 "shape": [8], "dtype": "bfloat16", "field_offset_bytes": 0,
868 "page_stride_bytes": 32},
869 {"group_id": "state", "field_id": "layer.1.state", "plane_id": "p1",
870 "shape": [8], "dtype": "bfloat16", "field_offset_bytes": 0,
871 "page_stride_bytes": 32}
872 ],
873 "resident_block_bytes": null
874 },
875 "group_specs": [
876 {"group_id": "history", "attn_kind": "full_history", "rows_per_page": 2,
877 "entry_stride_tokens": 1, "sliding_window_tokens": null, "replayable": false,
878 "transfer_policy": "full_suffix", "shard_count": 1},
879 {"group_id": "state", "attn_kind": "state", "rows_per_page": null,
880 "entry_stride_tokens": null, "sliding_window_tokens": null, "replayable": false,
881 "transfer_policy": "latest_snapshot", "shard_count": 1}
882 ],
883 "transfer_schema": {"fields": []},
884 "field_addresses": null
885 })
886 .to_string()
887 }
888
889 #[test]
890 fn a_contract_parses_and_answers_its_geometry() {
891 let contract = Contract::from_wire(wire().as_bytes()).unwrap();
892 assert_eq!(contract.virtual_block_count("history").unwrap(), 16);
893 assert_eq!(
894 contract.field_address(0x1000, "layer.1.state").unwrap(),
895 0x1000 + 16
896 );
897 assert_eq!(contract.field("layer.0.kv").unwrap().payload_bytes(), 16);
898 assert!(contract.group_spec("history").unwrap().full_suffix());
899 assert!(contract
900 .group_spec("state")
901 .unwrap()
902 .block_granularity()
903 .is_err());
904 }
905
906 #[test]
907 fn malformed_contracts_are_refused() {
908 let mutate = |edit: fn(&mut Value)| {
909 let mut value: Value = serde_json::from_str(&wire()).unwrap();
910 edit(&mut value);
911 Contract::from_wire(value.to_string().as_bytes())
912 };
913 assert!(mutate(|v| v["plan"]["fields"][0]["dtype"] = "float7".into()).is_err());
914 assert!(mutate(|v| v["plan"]["groups"][0]["extra"] = 1.into()).is_err());
915 assert!(mutate(|v| v["group_specs"][1]["rows_per_page"] = 2.into()).is_err());
916 assert!(mutate(|v| v["field_addresses"] = serde_json::json!({"layer.0.kv": 1})).is_err());
917 assert!(Contract::from_wire(b"").is_err());
918 assert!(Contract::from_wire(b"[]").is_err());
919 }
920}