Skip to main content

ts_pd/layout/
contract.rs

1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! The cache transfer contract (`pd/cache_protocol.py`'s
22//! `CacheTransferContract`): one rank's cache memory plan, its groups'
23//! semantics and its fields' TP partitioning, as the PD wire carries it.
24//!
25//! The Python cache recipes build the contract and serialize it once at
26//! startup; this side only parses it, checks it the way the Python
27//! constructors do, and answers the questions transfer planning asks.
28
29use std::collections::{BTreeMap, HashMap, HashSet};
30
31use serde_json::{Map, Value};
32
33use super::LayoutError;
34
35/// The largest contract the wire carries.
36pub const MAX_CONTRACT_WIRE_BYTES: usize = 256 << 10;
37
38/// A field's element width by dtype name (`plan._CACHE_DTYPE_BYTES`).
39fn 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/// How many CacheBlocks of one group a parent holds, and how many it has.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct GroupLayout {
52    /// The group.
53    pub group_id: String,
54    /// CacheBlocks per LCM parent.
55    pub cache_blocks_per_lcm_block: i64,
56    /// Local pages, the null page included.
57    pub page_count: i64,
58}
59
60/// One plane of the arena.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PlaneLayout {
63    /// The plane.
64    pub plane_id: String,
65    /// Its bytes per LCM parent.
66    pub bytes_per_lcm_block: i64,
67    /// Where it starts in the arena.
68    pub arena_offset_bytes: i64,
69}
70
71/// One field's geometry.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct FieldLayout {
74    /// Its group.
75    pub group_id: String,
76    /// The field (`layer.<id>.<plane>`).
77    pub field_id: String,
78    /// Its plane.
79    pub plane_id: String,
80    /// The rank-local shape of one page's payload.
81    pub shape: Vec<i64>,
82    /// The dtype the arena views it as.
83    pub dtype: String,
84    /// Its offset within a page of the plane.
85    pub field_offset_bytes: i64,
86    /// Bytes between consecutive pages.
87    pub page_stride_bytes: i64,
88}
89
90impl FieldLayout {
91    /// Bytes per element.
92    pub fn element_size(&self) -> i64 {
93        dtype_bytes(&self.dtype).expect("checked when parsed")
94    }
95
96    /// Bytes of one page's payload.
97    pub fn payload_bytes(&self) -> i64 {
98        self.shape.iter().product::<i64>() * self.element_size()
99    }
100}
101
102/// One cache group's scheduler-facing semantics (`CacheGroupSpec`).
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct GroupSpec {
105    /// The group.
106    pub group_id: String,
107    /// `full_history`, `sliding_window` or `state`.
108    pub attn_kind: String,
109    /// Rows per page; row-geometry groups only.
110    pub rows_per_page: Option<i64>,
111    /// Raw tokens per row; row-geometry groups only.
112    pub entry_stride_tokens: Option<i64>,
113    /// The retained window of a sliding-window group.
114    pub sliding_window_tokens: Option<i64>,
115    /// A sliding-window group regenerated by bounded replay.
116    pub replayable: bool,
117    /// `full_suffix` or `latest_snapshot`.
118    pub transfer_policy: Option<String>,
119    /// Cyclic owners of the group's virtual blocks; 1 is replicated.
120    pub shard_count: i64,
121}
122
123impl GroupSpec {
124    /// The raw-token span of one block-table slot (`block_granularity`);
125    /// a state group has none.
126    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    /// Whether the group transfers its retained history suffix (else its
137    /// latest snapshot).
138    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/// How one field's global extent splits over TP ranks
195/// (`CacheFieldPartition`).
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct Partition {
198    /// The partitioned axis of the local shape.
199    pub axis: i64,
200    /// Its global extent.
201    pub global_extent: i64,
202    /// Sub-ranges partitioned separately (a fused QKV), or empty.
203    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/// One rank's static cache byte geometry (`CacheMemoryPlan`).
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct Plan {
245    /// Tokens per prefix-matching unit.
246    pub prefix_granularity: i64,
247    /// Bytes of one LCM parent.
248    pub lcm_block_bytes: i64,
249    /// Parents, the null one excluded.
250    pub num_lcm_blocks: i64,
251    /// The groups.
252    pub groups: Vec<GroupLayout>,
253    /// The planes.
254    pub planes: Vec<PlaneLayout>,
255    /// The fields.
256    pub fields: Vec<FieldLayout>,
257    /// The retained planes' bytes per parent, when narrowed to a layer
258    /// window.
259    pub resident_block_bytes: Option<i64>,
260}
261
262/// A rank's cache transfer contract.
263#[derive(Debug, Clone)]
264pub struct Contract {
265    /// The memory plan.
266    pub plan: Plan,
267    /// The groups' semantics, in the scheduler's order.
268    pub group_specs: Vec<GroupSpec>,
269    /// The partitioned fields, by field.
270    partitions: HashMap<String, Partition>,
271    /// The fields' landing addresses, on an endpoint's contract; absent in
272    /// the rank-independent bootstrap layout.
273    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    /// Each group's fields, sorted by field id (`fields_for_group`).
279    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
293/// The keys a dataclass built from `**mapping` accepts: anything else, or a
294/// missing required key, is refused, as the Python constructor refuses it.
295fn 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
354/// A wire payload as a JSON object (`_load_wire_json`).
355pub(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    /// Parse a contract off the wire (`CacheTransferContract.from_wire_bytes`).
506    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    /// A contract from its parts, checked as the Python constructor checks
582    /// it.
583    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        // Later entries win, as in a Python dict.
606        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    /// The same contract without its landing addresses: the logical layout
670    /// every rank of a node registers with the bootstrap server.
671    pub fn without_addresses(&self) -> Self {
672        Self {
673            field_addresses: None,
674            ..self.clone()
675        }
676    }
677
678    /// The group's semantics.
679    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    /// The group's geometry.
687    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    /// A field's geometry.
695    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    /// The group's fields, sorted by field id (`fields_for_group`).
703    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    /// The group's cyclic owner count; one means replicated.
716    pub fn shard_count(&self, group_id: &str) -> Result<i64, LayoutError> {
717        Ok(self.group_spec(group_id)?.shard_count)
718    }
719
720    /// The exclusive bound of the scheduler block ids a manifest of the
721    /// group may carry (`virtual_block_count`).
722    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    /// A field's TP partition, if it has one.
728    pub fn partition_for(&self, field_id: &str) -> Option<&Partition> {
729        self.partitions.get(field_id)
730    }
731
732    /// One field page's byte offset in the arena (`field_page_byte_offset`).
733    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    /// Where a field's page 0 lands for an arena at `base`: its advertised
755    /// address on an endpoint's contract, else `base` plus its offset.
756    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
767/// The field's global shape: its local shape with the partitioned axis at
768/// its global extent.
769fn 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
782/// Check that a peer's contract describes the same cache semantically
783/// (`validate_cache_peer_layout`): the same prefix granularity, groups in
784/// the same order with the same semantics, and the same fields per group
785/// with the same dtype, partitioning and global shape. Shard counts may
786/// differ: each side shards its groups on its own.
787pub 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}