Skip to main content

xlog_cuda/semantic_transition/
learning_phase.rs

1//! Cold learning-phase transitions over the complete native model allocation map.
2//!
3//! The privileged native owner supplies its durably confirmed Admission and the
4//! complete copy/reset recipe. Scientific acceptance follows private execution
5//! and belongs outside its immutable checkpoint. This module owns phase writes,
6//! validates alias effects, and retains admission and ancestry; it never infers
7//! a phase from a tensor role or grant.
8
9use super::*;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum SemanticLearningPhase {
13    Alignment = 0,
14    Fast = 1,
15    Consolidation = 2,
16}
17
18impl SemanticLearningPhase {
19    pub fn from_code(code: u64) -> Result<Self, SemanticTransitionError> {
20        match code {
21            0 => Ok(Self::Alignment),
22            1 => Ok(Self::Fast),
23            2 => Ok(Self::Consolidation),
24            _ => Err(publication_input_error("unknown learning phase")),
25        }
26    }
27}
28
29/// One operation on an explicitly identified typed view. Full backing allocations,
30/// including padding, are copied first. No operation may silently change another
31/// view, even when the producer deliberately shares its physical storage.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum SemanticLearningCopyReset {
34    Preserve {
35        role: u64,
36        index: u64,
37    },
38    Zero {
39        role: u64,
40        index: u64,
41    },
42    MasterFromEffective {
43        index: u64,
44        effective_role: u64,
45        effective_index: u64,
46    },
47    Phase {
48        index: u64,
49    },
50}
51
52impl SemanticLearningCopyReset {
53    fn key(&self) -> (u64, u64) {
54        match *self {
55            Self::Preserve { role, index } | Self::Zero { role, index } => (role, index),
56            Self::MasterFromEffective { index, .. } => (21, index),
57            Self::Phase { index } => (23, index),
58        }
59    }
60}
61
62/// A transition request, not an assertion that a scientific criterion passed.
63/// The privileged Controller supplies its original signed, durably read-back
64/// Admission, separately from checking original data-use grants. This is permission
65/// for private work, never a claim that the scientific comparison passed.
66#[derive(Clone, Debug)]
67pub struct SemanticLearningPhaseTransition {
68    pub source: SemanticLearningPhase,
69    pub target: SemanticLearningPhase,
70    pub phase_index: u64,
71    pub completed_updates_index: u64,
72    pub recipe: Vec<SemanticLearningCopyReset>,
73    pub admission: Vec<u8>,
74}
75
76/// Retained evidence for one actual native cold phase change. Ordinary Update and
77/// restore preserve this history; neither can reset the cumulative progress.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct SemanticLearningPhaseRecord {
80    pub source: SemanticLearningPhase,
81    pub target: SemanticLearningPhase,
82    pub phase_index: u64,
83    pub completed_updates_index: u64,
84    pub predecessor: SemanticPublishedIdentity,
85    pub model_generation: u64,
86    pub completed_updates: u64,
87    pub recipe_digest: Identity256,
88    pub recipe: Vec<SemanticLearningCopyReset>,
89    pub admission: Vec<u8>,
90}
91
92impl SemanticLearningPhaseTransition {
93    pub fn recipe_digest(&self) -> Identity256 {
94        let mut digest = Sha256::new();
95        digest.update(b"xlog.learning-phase.copy-reset.v1\0");
96        for value in [
97            self.source as u64,
98            self.target as u64,
99            self.phase_index,
100            self.completed_updates_index,
101            self.recipe.len() as u64,
102        ] {
103            digest.update(value.to_le_bytes());
104        }
105        for operation in &self.recipe {
106            let (role, index) = operation.key();
107            let (code, source_role, source_index) = match *operation {
108                SemanticLearningCopyReset::Preserve { .. } => (0, 0, 0),
109                SemanticLearningCopyReset::Zero { .. } => (1, 0, 0),
110                SemanticLearningCopyReset::MasterFromEffective {
111                    effective_role,
112                    effective_index,
113                    ..
114                } => (2, effective_role, effective_index),
115                SemanticLearningCopyReset::Phase { .. } => (3, 0, 0),
116            };
117            for value in [role, index, code, source_role, source_index] {
118                digest.update(value.to_le_bytes());
119            }
120        }
121        Identity256::from_bytes(digest.finalize().into())
122    }
123
124    pub(super) fn apply(
125        &self,
126        material: &mut PublicationMaterial,
127    ) -> Result<(SemanticLearningPhaseRecord, Option<LearningFoldPlan>), SemanticTransitionError>
128    {
129        if !matches!(
130            (self.source, self.target),
131            (
132                SemanticLearningPhase::Alignment,
133                SemanticLearningPhase::Fast
134            ) | (
135                SemanticLearningPhase::Fast,
136                SemanticLearningPhase::Consolidation
137            ) | (
138                SemanticLearningPhase::Consolidation,
139                SemanticLearningPhase::Fast
140            )
141        ) || self.admission.is_empty()
142            || self.phase_index == self.completed_updates_index
143        {
144            return Err(publication_input_error(
145                "learning transition lacks its permitted boundary or original native admission",
146            ));
147        }
148        material.require_successful_recompute()?;
149        let phase = scalar_i64(material, (23, self.phase_index))?;
150        let updates = scalar_i64(material, (23, self.completed_updates_index))?;
151        if phase != self.source as i64 || updates < 0 {
152            return Err(publication_input_error(
153                "learning transition differs from the selected phase or cumulative progress",
154            ));
155        }
156        if let Some(previous) = material.learning_phases.last() {
157            if previous.target != self.source
158                || previous.phase_index != self.phase_index
159                || previous.completed_updates_index != self.completed_updates_index
160                || previous.completed_updates > updates as u64
161            {
162                return Err(publication_input_error(
163                    "learning transition changed its retained phase lineage",
164                ));
165            }
166        } else if self.source != SemanticLearningPhase::Alignment {
167            return Err(publication_input_error(
168                "learning lineage must begin with admitted alignment",
169            ));
170        }
171        let expected = material
172            .layouts
173            .keys()
174            .filter(|(role, _)| (18..=25).contains(role))
175            .copied()
176            .collect::<BTreeSet<_>>();
177        let keys = self
178            .recipe
179            .iter()
180            .map(SemanticLearningCopyReset::key)
181            .collect::<Vec<_>>();
182        if keys.windows(2).any(|pair| pair[0] >= pair[1])
183            || keys.iter().copied().collect::<BTreeSet<_>>() != expected
184            || !self.recipe.contains(&SemanticLearningCopyReset::Phase {
185                index: self.phase_index,
186            })
187            || !self.recipe.contains(&SemanticLearningCopyReset::Preserve {
188                role: 23,
189                index: self.completed_updates_index,
190            })
191        {
192            return Err(publication_input_error("copy/reset recipe must cover every model and learning view exactly once in native order"));
193        }
194        let fold = if self.source == SemanticLearningPhase::Fast
195            && self.target == SemanticLearningPhase::Consolidation
196        {
197            Some(LearningFoldPlan::prepare(material, self)?)
198        } else {
199            None
200        };
201        // Absorption reads the original effective factors on the device. In
202        // particular, its masters must not be reset from pre-absorption values.
203        if fold.is_none() {
204            let original = &material.model_allocations;
205            let mut allocations = original.clone();
206            for operation in &self.recipe {
207                if matches!(operation, SemanticLearningCopyReset::Preserve { .. }) {
208                    continue;
209                }
210                visit_expected_values(
211                    material,
212                    operation,
213                    self.target,
214                    self.phase_index,
215                    |allocation, offset, value| {
216                        allocations[allocation][offset..offset + value.len()]
217                            .copy_from_slice(value);
218                        Ok(())
219                    },
220                )?;
221            }
222            for operation in &self.recipe {
223                visit_expected_values(
224                    material,
225                    operation,
226                    self.target,
227                    self.phase_index,
228                    |allocation, offset, value| {
229                        if allocations[allocation][offset..offset + value.len()] != *value {
230                            return Err(publication_input_error(
231                            "copy/reset recipe has conflicting effects on shared physical views",
232                        ));
233                        }
234                        Ok(())
235                    },
236                )?;
237            }
238            material.model_allocations = allocations;
239            for range in &mut material.ranges {
240                if matches!(range.range.role, 18..=25) {
241                    let (allocation, offset) = material
242                        .model_memory
243                        .location(range.range.role, range.range.index)?;
244                    range.bytes = material.model_allocations[allocation]
245                        [offset..offset + range.bytes.len()]
246                        .to_vec();
247                }
248            }
249        }
250        let header = material.bank.header;
251        Ok((
252            SemanticLearningPhaseRecord {
253                source: self.source,
254                target: self.target,
255                phase_index: self.phase_index,
256                completed_updates_index: self.completed_updates_index,
257                predecessor: SemanticPublishedIdentity {
258                    instance: header.instance,
259                    word: header.publication_word,
260                    logical_digest: header.logical_digest,
261                    state_digest: header.state_digest,
262                },
263                model_generation: header.model_generation,
264                completed_updates: updates as u64,
265                recipe_digest: self.recipe_digest(),
266                recipe: self.recipe.clone(),
267                admission: self.admission.clone(),
268            },
269            fold,
270        ))
271    }
272}
273
274fn scalar_i64(
275    material: &PublicationMaterial,
276    key: (u64, u64),
277) -> Result<i64, SemanticTransitionError> {
278    let layout = material
279        .layouts
280        .get(&key)
281        .ok_or_else(|| publication_input_error("learning scalar is absent"))?;
282    if layout.rank != 0 || layout.scalar_type != 7 || layout.element_bytes != 8 {
283        return Err(publication_input_error(
284            "learning phase and progress need their actual I64 scalar views",
285        ));
286    }
287    let (allocation, offset) = material.model_memory.location(key.0, key.1)?;
288    let bytes = material.model_allocations[allocation]
289        .get(offset..offset + 8)
290        .ok_or(SemanticTransitionError::ObservationMismatch)?;
291    Ok(i64::from_le_bytes(bytes.try_into().map_err(|_| {
292        SemanticTransitionError::ObservationMismatch
293    })?))
294}
295
296fn element_offsets<'a>(
297    material: &'a PublicationMaterial,
298    key: (u64, u64),
299) -> Result<
300    (
301        usize,
302        impl Iterator<Item = Result<usize, SemanticTransitionError>> + 'a,
303    ),
304    SemanticTransitionError,
305> {
306    let layout = material
307        .layouts
308        .get(&key)
309        .ok_or(SemanticTransitionError::ObservationMismatch)?;
310    let (allocation, base) = material.model_memory.location(key.0, key.1)?;
311    let rank =
312        usize::try_from(layout.rank).map_err(|_| SemanticTransitionError::ObservationMismatch)?;
313    if rank > 4 {
314        return Err(SemanticTransitionError::ObservationMismatch);
315    }
316    let count = layout.dimensions[..rank]
317        .iter()
318        .try_fold(1u64, |n, &dimension| {
319            n.checked_mul(dimension)
320                .ok_or(SemanticTransitionError::GenerationExhausted)
321        })?;
322    let offsets = (0..count).map(move |mut position| {
323        let mut offset = base as u64;
324        for axis in (0..rank).rev() {
325            let coordinate = position % layout.dimensions[axis];
326            position /= layout.dimensions[axis];
327            offset = coordinate
328                .checked_mul(layout.strides_bytes[axis])
329                .and_then(|stride| offset.checked_add(stride))
330                .ok_or(SemanticTransitionError::GenerationExhausted)?;
331        }
332        let end = offset
333            .checked_add(layout.element_bytes)
334            .ok_or(SemanticTransitionError::GenerationExhausted)?;
335        if end > material.model_memory.allocation_bytes[allocation] {
336            return Err(SemanticTransitionError::ObservationMismatch);
337        }
338        usize::try_from(offset).map_err(|_| SemanticTransitionError::GenerationExhausted)
339    });
340    Ok((allocation, offsets))
341}
342
343fn visit_expected_values(
344    material: &PublicationMaterial,
345    operation: &SemanticLearningCopyReset,
346    target: SemanticLearningPhase,
347    phase_index: u64,
348    mut visit: impl FnMut(usize, usize, &[u8]) -> Result<(), SemanticTransitionError>,
349) -> Result<(), SemanticTransitionError> {
350    let key = operation.key();
351    let width = material.layouts[&key].element_bytes as usize;
352    if width == 0 || width > 8 {
353        return Err(SemanticTransitionError::ObservationMismatch);
354    }
355    let (allocation, offsets) = element_offsets(material, key)?;
356    let mut effective_offsets = None;
357    match *operation {
358        SemanticLearningCopyReset::Zero { role, .. } if !matches!(role, 21 | 24 | 25) => {
359            return Err(publication_input_error(
360                "phase reset cannot zero effective weights, masks, rates or loss scale",
361            ))
362        }
363        SemanticLearningCopyReset::Phase { index } if index != phase_index => {
364            return Err(publication_input_error(
365                "recipe writes another learning phase",
366            ))
367        }
368        SemanticLearningCopyReset::MasterFromEffective {
369            effective_role,
370            effective_index,
371            ..
372        } => {
373            let effective_key = (effective_role, effective_index);
374            let layout = &material.layouts[&key];
375            let effective = material.layouts.get(&effective_key).ok_or_else(|| {
376                publication_input_error("master reset has no original effective parameter")
377            })?;
378            if !(18..=20).contains(&effective_role)
379                || effective.scalar_type != 5
380                || effective.element_bytes != 2
381                || layout.scalar_type != 6
382                || width != 4
383                || effective.rank != layout.rank
384                || effective.dimensions != layout.dimensions
385            {
386                return Err(publication_input_error("master reset requires the actual matching BF16 effective and FP32 master views"));
387            }
388            effective_offsets = Some(element_offsets(material, effective_key)?);
389        }
390        _ => {}
391    }
392    let phase = (target as i64).to_le_bytes();
393    for offset in offsets {
394        let offset = offset?;
395        let mut converted = [0u8; 4];
396        let expected = match *operation {
397            SemanticLearningCopyReset::Preserve { .. } => {
398                &material.model_allocations[allocation][offset..offset + width]
399            }
400            SemanticLearningCopyReset::Zero { .. } => &[0u8; 8][..width],
401            SemanticLearningCopyReset::Phase { .. } => &phase,
402            SemanticLearningCopyReset::MasterFromEffective { .. } => {
403                let (source_allocation, offsets) = effective_offsets
404                    .as_mut()
405                    .ok_or(SemanticTransitionError::ObservationMismatch)?;
406                let offset = offsets
407                    .next()
408                    .ok_or(SemanticTransitionError::ObservationMismatch)??;
409                let bytes = &material.model_allocations[*source_allocation][offset..offset + 2];
410                let bits = u32::from(u16::from_le_bytes([bytes[0], bytes[1]])) << 16;
411                if !f32::from_bits(bits).is_finite() {
412                    return Err(publication_input_error(
413                        "master reset encountered a nonfinite accepted effective weight",
414                    ));
415                }
416                converted.copy_from_slice(&bits.to_le_bytes());
417                &converted
418            }
419        };
420        visit(allocation, offset, expected)?;
421    }
422    Ok(())
423}
424
425struct FoldAssignment {
426    key: (u64, u64),
427    mode: u64,
428    source: Option<(u64, u64)>,
429    factors: Option<((u64, u64), (u64, u64))>,
430}
431
432/// Derived only from the complete original ModelContract and native memory map.
433/// This is an ephemeral launch plan, not a new producer roster or wire format.
434pub(super) struct LearningFoldPlan {
435    assignments: Vec<FoldAssignment>,
436    scale_bits: u32,
437    phase: u64,
438}
439
440fn fold_error() -> SemanticTransitionError {
441    publication_input_error(
442        "adapter absorption differs from its original model contract or physical views",
443    )
444}
445
446fn schema_array(
447    value: &serde_json::Value,
448) -> Result<&Vec<serde_json::Value>, SemanticTransitionError> {
449    value.as_array().ok_or_else(fold_error)
450}
451
452fn tagged<'a>(
453    value: &'a serde_json::Value,
454    tag: &str,
455) -> Result<&'a serde_json::Value, SemanticTransitionError> {
456    let pair = schema_array(value)?;
457    if pair.len() != 2 || pair[0].as_str() != Some(tag) {
458        return Err(fold_error());
459    }
460    Ok(&pair[1])
461}
462
463fn original_float(value: &serde_json::Value) -> Result<f32, SemanticTransitionError> {
464    // Python's canonical float.hex encoding, not a caller-provided decimal or
465    // a replacement scale inferred from target names.
466    let text = tagged(value, "float")?.as_str().ok_or_else(fold_error)?;
467    let (negative, text) = text.strip_prefix('-').map_or((false, text), |s| (true, s));
468    let (mantissa, exponent) = text
469        .strip_prefix("0x")
470        .and_then(|s| s.split_once('p'))
471        .ok_or_else(fold_error)?;
472    let (whole, fraction) = mantissa.split_once('.').ok_or_else(fold_error)?;
473    if whole.len() != 1 || fraction.is_empty() || fraction.len() > 13 {
474        return Err(fold_error());
475    }
476    let significand =
477        u64::from_str_radix(&format!("{whole}{fraction}"), 16).map_err(|_| fold_error())?;
478    let exponent = exponent.parse::<i32>().map_err(|_| fold_error())?;
479    if !(-1022..=1023).contains(&exponent) || significand > (1u64 << 53) - 1 {
480        return Err(fold_error());
481    }
482    let value = (significand as f64 / (1u64 << (4 * fraction.len())) as f64) * 2f64.powi(exponent);
483    let value = if negative { -value } else { value } as f32;
484    if !value.is_finite() {
485        return Err(fold_error());
486    }
487    Ok(value)
488}
489
490fn view_key(
491    value: &serde_json::Value,
492    material: &PublicationMaterial,
493) -> Result<(u64, u64), SemanticTransitionError> {
494    let fields = schema_array(&value["layout"])?;
495    if fields.len() != 8 {
496        return Err(fold_error());
497    }
498    let word = |i: usize| fields[i].as_u64().ok_or_else(fold_error);
499    let key = (word(0)?, word(1)?);
500    let layout = material.layouts.get(&key).ok_or_else(fold_error)?;
501    if [
502        layout.role,
503        layout.index,
504        layout.element_bytes,
505        layout.scalar_type,
506        layout.rank,
507        layout.logical_axis,
508    ] != [word(0)?, word(1)?, word(2)?, word(3)?, word(4)?, word(5)?]
509        || !matches!(key.0, 18..=25)
510    {
511        return Err(fold_error());
512    }
513    for (field, expected) in [(6, layout.dimensions), (7, layout.strides_bytes)] {
514        let actual = schema_array(&fields[field])?;
515        if actual.len() != 4
516            || actual
517                .iter()
518                .zip(expected)
519                .any(|(v, n)| v.as_u64() != Some(n))
520        {
521            return Err(fold_error());
522        }
523    }
524    Ok(key)
525}
526
527impl LearningFoldPlan {
528    fn prepare(
529        material: &PublicationMaterial,
530        transition: &SemanticLearningPhaseTransition,
531    ) -> Result<Self, SemanticTransitionError> {
532        let record = material
533            .ranges
534            .iter()
535            .find(|r| (r.range.role, r.range.index) == (44, 0))
536            .ok_or_else(fold_error)?;
537        let schema = retained_model_schema(&record.bytes, material.contract.model_contract_layout)?
538            .ok_or_else(fold_error)?;
539        let model = &schema["model"];
540        let physical = model["physical"].as_object().ok_or_else(fold_error)?;
541        let learning = &model["learning"];
542        let views = schema_array(&learning["views"])?;
543        let keys = views
544            .iter()
545            .map(|v| view_key(v, material))
546            .collect::<Result<Vec<_>, _>>()?;
547        let expected = material
548            .layouts
549            .keys()
550            .filter(|(role, _)| matches!(role, 18..=25))
551            .copied()
552            .collect::<BTreeSet<_>>();
553        if keys.iter().copied().collect::<BTreeSet<_>>() != expected || keys.len() != expected.len()
554        {
555            return Err(fold_error());
556        }
557        let mut allocation_classes = BTreeMap::new();
558        let mut storage_classes = BTreeMap::new();
559        for (view, key) in views.iter().zip(&keys) {
560            let geometry = &view["geometry"];
561            let layout = material.layouts[key];
562            let native_view = material
563                .model_memory
564                .views
565                .iter()
566                .find(|v| (v.role, v.index) == *key)
567                .ok_or_else(fold_error)?;
568            let storage = &material.model_memory.storages[native_view.storage as usize];
569            let word = |name: &str| geometry[name].as_u64().ok_or_else(fold_error);
570            let allocation = geometry["allocation"].as_str().ok_or_else(fold_error)?;
571            let storage_class = geometry["storage"].as_str().ok_or_else(fold_error)?;
572            let dimensions = schema_array(&geometry["shape"])?;
573            let strides = schema_array(&geometry["stride"])?;
574            let dtype = match geometry["dtype"].as_str() {
575                Some("torch.uint8") => (1, 1),
576                Some("torch.uint32") => (2, 4),
577                Some("torch.uint64") => (3, 8),
578                Some("torch.float16") => (4, 2),
579                Some("torch.bfloat16") => (5, 2),
580                Some("torch.float32") => (6, 4),
581                Some("torch.int64") => (7, 8),
582                Some("torch.bool") => (8, 1),
583                _ => return Err(fold_error()),
584            };
585            if dimensions.len() != layout.rank as usize
586                || dtype != (layout.scalar_type, layout.element_bytes)
587                || strides.len() != dimensions.len()
588                || dimensions
589                    .iter()
590                    .zip(layout.dimensions)
591                    .any(|(d, n)| d.as_u64() != Some(n))
592                || strides.iter().zip(layout.strides_bytes).any(|(s, n)| {
593                    s.as_u64().and_then(|s| s.checked_mul(layout.element_bytes)) != Some(n)
594                })
595                || word("offset")?.checked_mul(layout.element_bytes)
596                    != Some(native_view.byte_offset)
597                || word("span_bytes")? != storage.span_bytes
598                || word("allocation_byte_offset")? != storage.byte_offset
599                || word("allocation_span_bytes")?
600                    != material.model_memory.allocation_bytes[storage.allocation as usize]
601                || allocation_classes
602                    .insert(allocation, storage.allocation)
603                    .is_some_and(|a| a != storage.allocation)
604                || storage_classes
605                    .insert(storage_class, native_view.storage)
606                    .is_some_and(|s| s != native_view.storage)
607            {
608                return Err(fold_error());
609            }
610        }
611        if allocation_classes
612            .values()
613            .copied()
614            .collect::<BTreeSet<_>>()
615            .len()
616            != allocation_classes.len()
617            || storage_classes
618                .values()
619                .copied()
620                .collect::<BTreeSet<_>>()
621                .len()
622                != storage_classes.len()
623        {
624            return Err(fold_error());
625        }
626        let key_at = |value: &serde_json::Value| {
627            value
628                .as_u64()
629                .and_then(|i| usize::try_from(i).ok())
630                .and_then(|i| keys.get(i))
631                .copied()
632                .ok_or_else(fold_error)
633        };
634        let mut aliases = BTreeMap::new();
635        let mut owner_aliases = BTreeMap::new();
636        let mut masters = BTreeMap::new();
637        let mut owners = BTreeSet::new();
638        if key_at(&learning["shared"]["phase"])? != (23, transition.phase_index)
639            || key_at(&learning["shared"]["completed_updates"])?
640                != (23, transition.completed_updates_index)
641        {
642            return Err(fold_error());
643        }
644        for leaf in schema_array(&learning["leaves"])? {
645            let key = key_at(&leaf["effective"])?;
646            let view = &views[leaf["effective"].as_u64().ok_or_else(fold_error)? as usize];
647            let owner = view["owner"].as_str().ok_or_else(fold_error)?;
648            let entry = physical.get(owner).ok_or_else(fold_error)?;
649            if !owners.insert(owner) {
650                return Err(fold_error());
651            }
652            if entry["kind"].as_str() != Some("parameter")
653                || entry["geometry"] != view["geometry"]
654                || entry["aliases"] != leaf["aliases"]
655                || !(18..=20).contains(&key.0)
656            {
657                return Err(fold_error());
658            }
659            let names = schema_array(&leaf["aliases"])?;
660            if names.is_empty() || owner_aliases.insert(key, names.clone()).is_some() {
661                return Err(fold_error());
662            }
663            for name in names {
664                if aliases
665                    .insert(name.as_str().ok_or_else(fold_error)?.to_owned(), key)
666                    .is_some()
667                {
668                    return Err(fold_error());
669                }
670            }
671            if !leaf["master"].is_null() && masters.insert(key_at(&leaf["master"])?, key).is_some()
672            {
673                return Err(fold_error());
674            }
675        }
676        for buffer in schema_array(&learning["buffers"])? {
677            let key = key_at(&buffer["tensor"])?;
678            let view = &views[buffer["tensor"].as_u64().ok_or_else(fold_error)? as usize];
679            let entry = physical
680                .get(view["owner"].as_str().ok_or_else(fold_error)?)
681                .ok_or_else(fold_error)?;
682            if !owners.insert(view["owner"].as_str().ok_or_else(fold_error)?) {
683                return Err(fold_error());
684            }
685            if entry["kind"].as_str() != Some("buffer")
686                || entry["geometry"] != view["geometry"]
687                || entry["aliases"] != buffer["aliases"]
688            {
689                return Err(fold_error());
690            }
691            for name in schema_array(&buffer["aliases"])? {
692                if aliases
693                    .insert(name.as_str().ok_or_else(fold_error)?.to_owned(), key)
694                    .is_some()
695                {
696                    return Err(fold_error());
697                }
698            }
699        }
700        if owners != physical.keys().map(String::as_str).collect::<BTreeSet<_>>() {
701            return Err(fold_error());
702        }
703        let state = schema_array(&model["roots"]["adapter"]["modules"][""][1])?;
704        let mut fields = BTreeMap::new();
705        for field in state {
706            let field = schema_array(field)?;
707            if field.len() != 2
708                || fields
709                    .insert(field[0].as_str().ok_or_else(fold_error)?, &field[1])
710                    .is_some()
711            {
712                return Err(fold_error());
713            }
714        }
715        let field = |name: &str| fields.get(name).copied().ok_or_else(fold_error);
716        let targets = schema_array(tagged(field("target_names")?, "tuple")?)?;
717        let shapes = schema_array(tagged(field("target_shapes")?, "tuple")?)?;
718        let rank = tagged(field("rank")?, "int")?
719            .as_u64()
720            .filter(|r| *r > 0)
721            .ok_or_else(fold_error)?;
722        let scale = original_float(field("scale")?)?;
723        if targets.is_empty()
724            || targets.len() != shapes.len()
725            || original_float(field("dropout")?)? != 0.0
726            || tagged(field("initialization_identity")?, "str")?
727                .as_str()
728                .is_none_or(str::is_empty)
729        {
730            return Err(fold_error());
731        }
732        let mut assignments = Vec::new();
733        for operation in &transition.recipe {
734            let key = operation.key();
735            let source = match *operation {
736                SemanticLearningCopyReset::MasterFromEffective {
737                    effective_role,
738                    effective_index,
739                    ..
740                } => {
741                    let effective = (effective_role, effective_index);
742                    if masters.get(&key) != Some(&effective) {
743                        return Err(fold_error());
744                    }
745                    let layout = material.layouts[&key];
746                    let original = material.layouts.get(&effective).ok_or_else(fold_error)?;
747                    if key.0 != 21
748                        || layout.scalar_type != 6
749                        || layout.element_bytes != 4
750                        || original.scalar_type != 5
751                        || original.element_bytes != 2
752                        || layout.rank != original.rank
753                        || layout.dimensions != original.dimensions
754                    {
755                        return Err(fold_error());
756                    }
757                    Some(effective)
758                }
759                SemanticLearningCopyReset::Zero { role, .. } if !matches!(role, 21 | 24 | 25) => {
760                    return Err(fold_error())
761                }
762                SemanticLearningCopyReset::Phase { index } if index != transition.phase_index => {
763                    return Err(fold_error())
764                }
765                _ => None,
766            };
767            assignments.push(FoldAssignment {
768                key,
769                mode: match operation {
770                    SemanticLearningCopyReset::Preserve { .. } => 0,
771                    SemanticLearningCopyReset::Zero { .. } => 1,
772                    SemanticLearningCopyReset::MasterFromEffective { .. } => 2,
773                    SemanticLearningCopyReset::Phase { .. } => 3,
774                },
775                source,
776                factors: None,
777            });
778        }
779        if masters
780            .keys()
781            .any(|key| !assignments.iter().any(|a| a.key == *key && a.mode == 2))
782        {
783            return Err(fold_error());
784        }
785        for leaf in schema_array(&learning["leaves"])? {
786            for field in ["m", "v", "t", "gradient", "presence"] {
787                if !leaf[field].is_null() {
788                    let key = key_at(&leaf[field])?;
789                    if !assignments.iter().any(|a| a.key == key && a.mode == 1) {
790                        return Err(fold_error());
791                    }
792                }
793            }
794        }
795        let accumulation = key_at(&learning["shared"]["accumulation"])?;
796        if !assignments
797            .iter()
798            .any(|a| a.key == accumulation && a.mode == 1)
799        {
800            return Err(fold_error());
801        }
802        let mut replaced = BTreeSet::new();
803        let mut adapted_aliases = BTreeSet::new();
804        let mut previous = None;
805        for (index, (target, shape)) in targets.iter().zip(shapes).enumerate() {
806            let target = tagged(target, "str")?.as_str().ok_or_else(fold_error)?;
807            if previous.is_some_and(|p| p >= target) {
808                return Err(fold_error());
809            }
810            previous = Some(target);
811            let shape = schema_array(tagged(shape, "tuple")?)?;
812            if shape.len() != 2 {
813                return Err(fold_error());
814            }
815            let rows = tagged(&shape[0], "int")?
816                .as_u64()
817                .filter(|n| *n > 0)
818                .ok_or_else(fold_error)?;
819            let columns = tagged(&shape[1], "int")?
820                .as_u64()
821                .filter(|n| *n > 0)
822                .ok_or_else(fold_error)?;
823            let names = [
824                format!("model.{target}.weight"),
825                format!("adapter.residuals.{index}.down"),
826                format!("adapter.residuals.{index}.up"),
827                format!("adapter.residuals.{index}.neutral_down"),
828            ];
829            let actual = names
830                .iter()
831                .map(|name| aliases.get(name).copied().ok_or_else(fold_error))
832                .collect::<Result<Vec<_>, _>>()?;
833            let [weight, down, up, neutral] = actual.as_slice() else {
834                return Err(fold_error());
835            };
836            for (key, dims, role) in [
837                (*weight, [rows, columns, 0, 0], 18),
838                (*down, [rank, columns, 0, 0], 19),
839                (*up, [rows, rank, 0, 0], 19),
840                (*neutral, [rank, columns, 0, 0], 19),
841            ] {
842                let layout = material.layouts[&key];
843                if key.0 != role
844                    || layout.rank != 2
845                    || layout.dimensions != dims
846                    || !matches!((layout.scalar_type, layout.element_bytes), (5, 2) | (6, 4))
847                    || (key == *neutral && layout.scalar_type != material.layouts[down].scalar_type)
848                    || !transition
849                        .recipe
850                        .contains(&SemanticLearningCopyReset::Preserve {
851                            role: key.0,
852                            index: key.1,
853                        })
854                {
855                    return Err(fold_error());
856                }
857            }
858            for key in [*weight, *down, *up] {
859                replaced.insert(key);
860            }
861            for name in &names[..3] {
862                adapted_aliases.insert(name.clone());
863            }
864            assignments.push(FoldAssignment {
865                key: *weight,
866                mode: 4,
867                source: None,
868                factors: Some((*up, *down)),
869            });
870            assignments.push(FoldAssignment {
871                key: *down,
872                mode: 5,
873                source: Some(*neutral),
874                factors: None,
875            });
876            assignments.push(FoldAssignment {
877                key: *up,
878                mode: 1,
879                source: None,
880                factors: None,
881            });
882        }
883        assignments.retain(|a| {
884            a.mode != 0
885                || !replaced.contains(&a.key)
886                || owner_aliases.get(&a.key).is_some_and(|names| {
887                    names
888                        .iter()
889                        .any(|n| n.as_str().is_none_or(|n| !adapted_aliases.contains(n)))
890                })
891        });
892        Ok(Self {
893            assignments,
894            scale_bits: scale.to_bits(),
895            phase: transition.target as u64,
896        })
897    }
898}
899
900#[repr(C)]
901#[derive(Clone, Copy, Default)]
902pub(super) struct FoldDeviceAssignment {
903    original: u64,
904    scratch: u64,
905    expected: u64,
906    source: u64,
907    up: u64,
908    down: u64,
909    count: u64,
910    mode: u64,
911    phase: u64,
912    scale_bits: u64,
913    serial_scatter: u64,
914    layout: SemanticTensorLayout,
915    source_layout: SemanticTensorLayout,
916    up_layout: SemanticTensorLayout,
917    down_layout: SemanticTensorLayout,
918}
919
920unsafe impl DeviceRepr for FoldDeviceAssignment {}
921const _: () = assert!(size_of::<FoldDeviceAssignment>() == 536);
922
923fn packed_layout(
924    mut layout: SemanticTensorLayout,
925) -> Result<SemanticTensorLayout, SemanticTransitionError> {
926    let mut stride = layout.element_bytes;
927    for axis in (0..layout.rank as usize).rev() {
928        layout.strides_bytes[axis] = stride;
929        stride = stride
930            .checked_mul(layout.dimensions[axis])
931            .ok_or(SemanticTransitionError::GenerationExhausted)?;
932    }
933    Ok(layout)
934}
935
936impl SemanticTransitionSession {
937    pub(super) fn apply_learning_fold(
938        &mut self,
939        plan: &LearningFoldPlan,
940    ) -> Result<(), SemanticTransitionError> {
941        let storage = Arc::clone(
942            self.publication
943                .as_ref()
944                .ok_or(SemanticTransitionError::NotBound)?,
945        );
946        let pointer = |key: (u64, u64), bank: usize| {
947            let (allocation, offset) = storage.model_memory.location(key.0, key.1)?;
948            storage.allocations[storage.model_slots[allocation][bank]]
949                .device_ptr_value()
950                .checked_add(offset as u64)
951                .ok_or(SemanticTransitionError::GenerationExhausted)
952        };
953        let mut outputs = Vec::new();
954        let mut effective_outputs = BTreeMap::new();
955        for assignment in &plan.assignments {
956            let layout = storage.layouts[&assignment.key];
957            let count =
958                layout.dimensions[..layout.rank as usize]
959                    .iter()
960                    .try_fold(1u64, |n, &d| {
961                        n.checked_mul(d)
962                            .ok_or(SemanticTransitionError::GenerationExhausted)
963                    })?;
964            let bytes = count
965                .checked_mul(layout.element_bytes)
966                .and_then(|n| usize::try_from(n).ok())
967                .ok_or(SemanticTransitionError::GenerationExhausted)?;
968            let output = if assignment.mode == 0 || bytes == 0 {
969                None
970            } else {
971                Some(allocate_publication::<u8>(&self.provider, bytes)?)
972            };
973            if matches!(assignment.mode, 1 | 4 | 5) {
974                effective_outputs.entry(assignment.key).or_insert_with(|| {
975                    output
976                        .as_ref()
977                        .map_or(0, TrackedCudaSlice::device_ptr_value)
978                });
979            }
980            outputs.push(output);
981        }
982        let mut descriptors = Vec::new();
983        for (assignment, output) in plan.assignments.iter().zip(&outputs) {
984            let layout = storage.layouts[&assignment.key];
985            let mut descriptor = FoldDeviceAssignment {
986                original: pointer(assignment.key, 0)?,
987                scratch: pointer(assignment.key, 1)?,
988                expected: output
989                    .as_ref()
990                    .map_or(0, TrackedCudaSlice::device_ptr_value),
991                mode: assignment.mode,
992                phase: plan.phase,
993                scale_bits: u64::from(plan.scale_bits),
994                layout,
995                ..Default::default()
996            };
997            descriptor.count =
998                layout.dimensions[..layout.rank as usize]
999                    .iter()
1000                    .try_fold(1u64, |n, &d| {
1001                        n.checked_mul(d)
1002                            .ok_or(SemanticTransitionError::GenerationExhausted)
1003                    })?;
1004            if let Some(source) = assignment.source {
1005                descriptor.source_layout = storage.layouts[&source];
1006                if assignment.mode == 2 && effective_outputs.contains_key(&source) {
1007                    descriptor.source = effective_outputs[&source];
1008                    descriptor.source_layout = packed_layout(descriptor.source_layout)?;
1009                } else {
1010                    descriptor.source = pointer(source, 0)?;
1011                }
1012            }
1013            if let Some((up, down)) = assignment.factors {
1014                descriptor.up = pointer(up, 0)?;
1015                descriptor.down = pointer(down, 0)?;
1016                descriptor.up_layout = storage.layouts[&up];
1017                descriptor.down_layout = storage.layouts[&down];
1018            }
1019            // Parallel scatter is safe only for provably disjoint cells. A
1020            // deliberately overlapping stride uses the same byte-exact law in
1021            // serial order; the subsequent all-view check still detects conflict.
1022            let mut axes = (0..layout.rank as usize)
1023                .filter(|&i| layout.dimensions[i] > 1)
1024                .collect::<Vec<_>>();
1025            axes.sort_unstable_by_key(|&i| layout.strides_bytes[i]);
1026            let mut span = layout.element_bytes;
1027            for axis in axes {
1028                if layout.strides_bytes[axis] < span {
1029                    descriptor.serial_scatter = 1;
1030                }
1031                span = span
1032                    .checked_add(
1033                        (layout.dimensions[axis] - 1)
1034                            .checked_mul(layout.strides_bytes[axis])
1035                            .ok_or(SemanticTransitionError::GenerationExhausted)?,
1036                    )
1037                    .ok_or(SemanticTransitionError::GenerationExhausted)?;
1038            }
1039            descriptors.push(descriptor);
1040        }
1041        let status = allocate_publication::<u64>(&self.provider, 1)?;
1042        upload_publication(&self.provider, &[0u64], &status)?;
1043        let execute = self
1044            .provider
1045            .device()
1046            .inner()
1047            .get_func("xlog_semantic_transition", "semantic_learning_fold")
1048            .ok_or_else(|| runtime_error("kernel lookup", "learning absorption unavailable"))?;
1049        let mut recorder = self.domain.new_strict_recorder();
1050        storage.record(&mut recorder);
1051        recorder.read_write(&status);
1052        for output in outputs.iter().flatten() {
1053            recorder.read_write(output);
1054        }
1055        enqueue_recorded(&self.domain, &mut self.poisoned, recorder, |enqueue| {
1056            // Every expectation is computed before scatter. Masters consume the
1057            // already-rounded effective outputs, never an unrounded accumulator.
1058            for stage in 0..4u64 {
1059                for descriptor in &descriptors {
1060                    if descriptor.count == 0
1061                        || (stage == 0 && matches!(descriptor.mode, 0 | 2))
1062                        || (stage == 1 && descriptor.mode != 2)
1063                        || (stage == 2 && descriptor.mode == 0)
1064                    {
1065                        continue;
1066                    }
1067                    let blocks = descriptor.count.div_ceil(256).min(65535) as u32;
1068                    // SAFETY: original, scratch, expectation and all factor
1069                    // spans are derived from the complete retained native map;
1070                    // their owners were recorded before this enqueue boundary.
1071                    unsafe {
1072                        execute.clone().launch_in(
1073                            enqueue,
1074                            LaunchConfig {
1075                                grid_dim: (blocks, 1, 1),
1076                                block_dim: (256, 1, 1),
1077                                shared_mem_bytes: 0,
1078                            },
1079                            (*descriptor, stage, status.device_ptr_value()),
1080                        )
1081                    }
1082                    .map_err(|error| XlogError::Kernel(error.to_string()))?;
1083                }
1084            }
1085            Ok::<(), XlogError>(())
1086        })?;
1087        wait_on_stream(
1088            &self.stream,
1089            &mut self.poisoned,
1090            &mut self.stream_waits,
1091            "cold adapter absorption and physical alias check",
1092            CudaStream::synchronize,
1093        )?;
1094        if self.publication_read(status.view())?[0] != 0 {
1095            return Err(publication_input_error("adapter absorption encountered nonfinite numerics or conflicting physical alias bytes"));
1096        }
1097        let mut recorder = self.domain.new_strict_recorder();
1098        storage.record(&mut recorder);
1099        enqueue_recorded(&self.domain, &mut self.poisoned, recorder, |enqueue| {
1100            for slots in &storage.model_slots {
1101                let destination = &storage.allocations[slots[0]];
1102                if destination.is_empty() {
1103                    continue;
1104                }
1105                // SAFETY: the complete unsealed scratch allocation passed every
1106                // original view before this first candidate-bank assignment.
1107                unsafe {
1108                    sys::cuMemcpyDtoDAsync_v2(
1109                        destination.device_ptr_value(),
1110                        storage.allocations[slots[1]].device_ptr_value(),
1111                        destination.len(),
1112                        enqueue.stream().cu_stream(),
1113                    )
1114                }
1115                .result()
1116                .map_err(|error| XlogError::Kernel(error.to_string()))?;
1117            }
1118            Ok::<(), XlogError>(())
1119        })?;
1120        wait_on_stream(
1121            &self.stream,
1122            &mut self.poisoned,
1123            &mut self.stream_waits,
1124            "cold absorbed candidate assignment",
1125            CudaStream::synchronize,
1126        )
1127    }
1128}
1129
1130pub(super) fn encode_history(
1131    history: &[SemanticLearningPhaseRecord],
1132    bytes: &mut Vec<u8>,
1133) -> Result<(), SemanticTransitionError> {
1134    material_u32(
1135        bytes,
1136        u32::try_from(history.len()).map_err(|_| SemanticTransitionError::GenerationExhausted)?,
1137    );
1138    for record in history {
1139        for value in [
1140            record.source as u64,
1141            record.target as u64,
1142            record.phase_index,
1143            record.completed_updates_index,
1144            record.predecessor.word,
1145            record.model_generation,
1146            record.completed_updates,
1147        ] {
1148            material_u64(bytes, value);
1149        }
1150        for digest in [
1151            record.predecessor.instance,
1152            record.predecessor.logical_digest,
1153            record.predecessor.state_digest,
1154            record.recipe_digest,
1155        ] {
1156            bytes.extend_from_slice(digest.as_bytes());
1157        }
1158        material_u32(
1159            bytes,
1160            u32::try_from(record.recipe.len())
1161                .map_err(|_| SemanticTransitionError::GenerationExhausted)?,
1162        );
1163        for operation in &record.recipe {
1164            let (role, index) = operation.key();
1165            let (code, source_role, source_index) = match *operation {
1166                SemanticLearningCopyReset::Preserve { .. } => (0, 0, 0),
1167                SemanticLearningCopyReset::Zero { .. } => (1, 0, 0),
1168                SemanticLearningCopyReset::MasterFromEffective {
1169                    effective_role,
1170                    effective_index,
1171                    ..
1172                } => (2, effective_role, effective_index),
1173                SemanticLearningCopyReset::Phase { .. } => (3, 0, 0),
1174            };
1175            for value in [role, index, code, source_role, source_index] {
1176                material_u64(bytes, value);
1177            }
1178        }
1179        material_bytes(bytes, &record.admission).map_err(SemanticTransitionError::Semantic)?;
1180    }
1181    Ok(())
1182}
1183
1184pub(super) fn decode_history(
1185    reader: &mut SemanticMaterialReader<'_>,
1186) -> Result<Vec<SemanticLearningPhaseRecord>, SemanticTransitionError> {
1187    let count = reader
1188        .count(7 * 8 + 4 * 32 + 4)
1189        .map_err(SemanticTransitionError::Semantic)?;
1190    let mut history = Vec::with_capacity(count);
1191    for _ in 0..count {
1192        let mut next = || reader.u64().map_err(SemanticTransitionError::Semantic);
1193        let source = SemanticLearningPhase::from_code(next()?)?;
1194        let target = SemanticLearningPhase::from_code(next()?)?;
1195        let phase_index = next()?;
1196        let completed_updates_index = next()?;
1197        let word = next()?;
1198        let model_generation = next()?;
1199        let completed_updates = next()?;
1200        let mut identity = || -> Result<Identity256, SemanticTransitionError> {
1201            Ok(Identity256::from_bytes(
1202                reader
1203                    .take(32)
1204                    .map_err(SemanticTransitionError::Semantic)?
1205                    .try_into()
1206                    .map_err(|_| SemanticTransitionError::ObservationMismatch)?,
1207            ))
1208        };
1209        let instance = identity()?;
1210        let logical_digest = identity()?;
1211        let state_digest = identity()?;
1212        let recipe_digest = identity()?;
1213        let recipe_count = reader
1214            .count(5 * 8)
1215            .map_err(SemanticTransitionError::Semantic)?;
1216        let mut recipe = Vec::with_capacity(recipe_count);
1217        for _ in 0..recipe_count {
1218            let role = reader.u64().map_err(SemanticTransitionError::Semantic)?;
1219            let index = reader.u64().map_err(SemanticTransitionError::Semantic)?;
1220            let code = reader.u64().map_err(SemanticTransitionError::Semantic)?;
1221            let source_role = reader.u64().map_err(SemanticTransitionError::Semantic)?;
1222            let source_index = reader.u64().map_err(SemanticTransitionError::Semantic)?;
1223            recipe.push(match code {
1224                0 if source_role == 0 && source_index == 0 => {
1225                    SemanticLearningCopyReset::Preserve { role, index }
1226                }
1227                1 if source_role == 0 && source_index == 0 => {
1228                    SemanticLearningCopyReset::Zero { role, index }
1229                }
1230                2 if role == 21 => SemanticLearningCopyReset::MasterFromEffective {
1231                    index,
1232                    effective_role: source_role,
1233                    effective_index: source_index,
1234                },
1235                3 if role == 23 && source_role == 0 && source_index == 0 => {
1236                    SemanticLearningCopyReset::Phase { index }
1237                }
1238                _ => {
1239                    return Err(publication_input_error(
1240                        "checkpoint learning recipe has an invalid operation",
1241                    ))
1242                }
1243            });
1244        }
1245        let admission = reader
1246            .bytes()
1247            .map_err(SemanticTransitionError::Semantic)?
1248            .to_vec();
1249        history.push(SemanticLearningPhaseRecord {
1250            source,
1251            target,
1252            phase_index,
1253            completed_updates_index,
1254            predecessor: SemanticPublishedIdentity {
1255                instance,
1256                word,
1257                logical_digest,
1258                state_digest,
1259            },
1260            model_generation,
1261            completed_updates,
1262            recipe_digest,
1263            recipe,
1264            admission,
1265        });
1266    }
1267    Ok(history)
1268}
1269
1270pub(super) fn validate_history(
1271    material: &PublicationMaterial,
1272) -> Result<(), SemanticTransitionError> {
1273    let mut previous: Option<&SemanticLearningPhaseRecord> = None;
1274    for record in &material.learning_phases {
1275        let keys = record
1276            .recipe
1277            .iter()
1278            .map(SemanticLearningCopyReset::key)
1279            .collect::<Vec<_>>();
1280        let recipe = SemanticLearningPhaseTransition {
1281            source: record.source,
1282            target: record.target,
1283            phase_index: record.phase_index,
1284            completed_updates_index: record.completed_updates_index,
1285            recipe: record.recipe.clone(),
1286            admission: record.admission.clone(),
1287        };
1288        if record.admission.is_empty()
1289            || record.phase_index == record.completed_updates_index
1290            || keys.windows(2).any(|pair| pair[0] >= pair[1])
1291            || keys.iter().any(|(role, _)| !(18..=25).contains(role))
1292            || !record.recipe.contains(&SemanticLearningCopyReset::Phase {
1293                index: record.phase_index,
1294            })
1295            || !record
1296                .recipe
1297                .contains(&SemanticLearningCopyReset::Preserve {
1298                    role: 23,
1299                    index: record.completed_updates_index,
1300                })
1301            || record.recipe.iter().any(|operation| match operation {
1302                SemanticLearningCopyReset::Zero { role, .. } => !matches!(role, 21 | 24 | 25),
1303                SemanticLearningCopyReset::MasterFromEffective { effective_role, .. } => {
1304                    !(18..=20).contains(effective_role)
1305                }
1306                SemanticLearningCopyReset::Phase { index } => *index != record.phase_index,
1307                SemanticLearningCopyReset::Preserve { .. } => false,
1308            })
1309            || record.completed_updates > i64::MAX as u64
1310            || record.model_generation == 0
1311            || record.model_generation > material.bank.header.model_generation
1312            || record.predecessor.instance == Identity256::default()
1313            || record.recipe_digest == Identity256::default()
1314            || record.recipe_digest != recipe.recipe_digest()
1315            || !matches!(
1316                (record.source, record.target),
1317                (
1318                    SemanticLearningPhase::Alignment,
1319                    SemanticLearningPhase::Fast
1320                ) | (
1321                    SemanticLearningPhase::Fast,
1322                    SemanticLearningPhase::Consolidation
1323                ) | (
1324                    SemanticLearningPhase::Consolidation,
1325                    SemanticLearningPhase::Fast
1326                )
1327            )
1328            || previous.is_none() && record.source != SemanticLearningPhase::Alignment
1329            || previous.is_some_and(|prior| {
1330                prior.target != record.source
1331                    || prior.phase_index != record.phase_index
1332                    || prior.completed_updates_index != record.completed_updates_index
1333                    || prior.completed_updates > record.completed_updates
1334                    || prior.model_generation > record.model_generation
1335            })
1336        {
1337            return Err(publication_input_error(
1338                "checkpoint learning-phase lineage is inconsistent",
1339            ));
1340        }
1341        previous = Some(record);
1342    }
1343    if let Some(record) = previous {
1344        if scalar_i64(material, (23, record.phase_index))? != record.target as i64
1345            || scalar_i64(material, (23, record.completed_updates_index))?
1346                < record.completed_updates as i64
1347        {
1348            return Err(publication_input_error(
1349                "selected learning values differ from their retained phase lineage",
1350            ));
1351        }
1352    }
1353    Ok(())
1354}