ferrum_interfaces/vnext/execution/
checkpoint.rs1use super::{
2 invalid_plan, BTreeSet, BufferUsage, Deserialize, NodeId, PlanNode, PreparedModelFamily,
3 ProgramValueId, ResolvedTensorLayout, ResolvedTensorSpec, ResolvedValueBinding,
4 ResolvedValueRole, ResourceId, Serialize, TensorAccess, VNextError,
5};
6use crate::vnext::{CompletionReadbackRequest, HostTransferLayout};
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct CompletionRetentionSpec {
12 values: BTreeSet<ProgramValueId>,
13}
14
15impl CompletionRetentionSpec {
16 pub fn new(values: BTreeSet<ProgramValueId>) -> Self {
17 Self { values }
18 }
19
20 pub fn for_determinism_outputs(family: &PreparedModelFamily) -> Result<Self, VNextError> {
24 let mut values = BTreeSet::new();
25 for node in family
26 .program()
27 .blocks()
28 .iter()
29 .flat_map(|block| &block.nodes)
30 {
31 for value_id in &node.outputs {
32 if !values.insert(value_id.clone()) {
33 return Err(invalid_plan(format!(
34 "determinism output `{value_id}` has more than one producer"
35 )));
36 }
37 }
38 }
39 if values.is_empty() {
40 return Err(invalid_plan(
41 "determinism output retention requires at least one operation output",
42 ));
43 }
44 Ok(Self { values })
45 }
46
47 pub fn insert(&mut self, value_id: ProgramValueId) -> bool {
48 self.values.insert(value_id)
49 }
50
51 pub fn values(&self) -> &BTreeSet<ProgramValueId> {
52 &self.values
53 }
54
55 pub fn is_empty(&self) -> bool {
56 self.values.is_empty()
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64pub struct RetainedCompletionValue {
65 value_id: ProgramValueId,
66 producer_node_id: NodeId,
67 output_ordinal: u32,
68 resource_id: ResourceId,
69 logical_offset_bytes: u64,
70 tensor: ResolvedTensorSpec,
71}
72
73#[derive(Deserialize)]
74#[serde(deny_unknown_fields)]
75struct RetainedCompletionValueWire {
76 value_id: ProgramValueId,
77 producer_node_id: NodeId,
78 output_ordinal: u32,
79 resource_id: ResourceId,
80 logical_offset_bytes: u64,
81 tensor: ResolvedTensorSpec,
82}
83
84impl<'de> Deserialize<'de> for RetainedCompletionValue {
85 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86 where
87 D: serde::Deserializer<'de>,
88 {
89 let wire = RetainedCompletionValueWire::deserialize(deserializer)?;
90 Self::new(
91 wire.value_id,
92 wire.producer_node_id,
93 wire.output_ordinal,
94 wire.resource_id,
95 wire.logical_offset_bytes,
96 wire.tensor,
97 )
98 .map_err(serde::de::Error::custom)
99 }
100}
101
102impl RetainedCompletionValue {
103 fn new(
104 value_id: ProgramValueId,
105 producer_node_id: NodeId,
106 output_ordinal: u32,
107 resource_id: ResourceId,
108 logical_offset_bytes: u64,
109 tensor: ResolvedTensorSpec,
110 ) -> Result<Self, VNextError> {
111 let byte_len = tensor.minimum_storage_bytes()?;
112 if logical_offset_bytes.checked_add(byte_len).is_none() {
113 return Err(invalid_plan(
114 "retained completion value range overflows u64",
115 ));
116 }
117 Ok(Self {
118 value_id,
119 producer_node_id,
120 output_ordinal,
121 resource_id,
122 logical_offset_bytes,
123 tensor,
124 })
125 }
126
127 pub fn value_id(&self) -> &ProgramValueId {
128 &self.value_id
129 }
130
131 pub fn producer_node_id(&self) -> &NodeId {
132 &self.producer_node_id
133 }
134
135 pub const fn output_ordinal(&self) -> u32 {
136 self.output_ordinal
137 }
138
139 pub fn resource_id(&self) -> &ResourceId {
140 &self.resource_id
141 }
142
143 pub const fn logical_offset_bytes(&self) -> u64 {
144 self.logical_offset_bytes
145 }
146
147 pub fn tensor(&self) -> &ResolvedTensorSpec {
148 &self.tensor
149 }
150
151 pub fn readback_request(
152 &self,
153 participant_index: u32,
154 output_layout: HostTransferLayout,
155 ) -> Result<CompletionReadbackRequest, VNextError> {
156 if !matches!(self.tensor.layout(), ResolvedTensorLayout::Contiguous) {
157 return Err(invalid_plan(
158 "completion readback currently requires contiguous retained storage",
159 ));
160 }
161 if output_layout.element_type() != self.tensor.element_type() {
162 return Err(invalid_plan(
163 "completion readback element type differs from retained activation",
164 ));
165 }
166 if output_layout.byte_len()? > self.tensor.minimum_storage_bytes()? {
167 return Err(invalid_plan(
168 "completion readback exceeds retained activation capacity",
169 ));
170 }
171 CompletionReadbackRequest::new(
172 self.producer_node_id.clone(),
173 participant_index,
174 self.resource_id.clone(),
175 self.logical_offset_bytes,
176 output_layout,
177 )
178 }
179}
180
181pub(super) fn resolve_retained_completion_values(
182 nodes: &[PlanNode],
183 spec: &CompletionRetentionSpec,
184) -> Result<Vec<RetainedCompletionValue>, VNextError> {
185 let mut retained = Vec::with_capacity(spec.values.len());
186 for value_id in &spec.values {
187 let matches = nodes
188 .iter()
189 .enumerate()
190 .flat_map(|(node_index, node)| {
191 node.values()
192 .iter()
193 .map(move |binding| (node_index, node, binding))
194 })
195 .filter(|(_, _, binding)| {
196 binding.value_id() == value_id && binding.role() == ResolvedValueRole::Output
197 })
198 .collect::<Vec<_>>();
199 let [(producer_index, producer, binding)] = matches.as_slice() else {
200 return Err(invalid_plan(format!(
201 "completion retention value `{value_id}` must identify exactly one plan-node output"
202 )));
203 };
204 if binding.usage() != BufferUsage::Activations {
205 return Err(invalid_plan(format!(
206 "completion retention value `{value_id}` is not an activation"
207 )));
208 }
209 let [component] = binding.storage().components() else {
210 return Err(invalid_plan(format!(
211 "completion retention value `{value_id}` does not use one physical component"
212 )));
213 };
214 let retained_end = component
215 .offset_bytes()
216 .checked_add(binding.tensor().minimum_storage_bytes()?)
217 .ok_or_else(|| invalid_plan("retained completion range overflows u64"))?;
218 reject_later_overlapping_write(
219 nodes,
220 *producer_index,
221 binding,
222 component.resource_id(),
223 component.offset_bytes(),
224 retained_end,
225 )?;
226 retained.push(RetainedCompletionValue::new(
227 value_id.clone(),
228 producer.id().clone(),
229 binding.ordinal(),
230 component.resource_id().clone(),
231 component.offset_bytes(),
232 binding.tensor().clone(),
233 )?);
234 }
235 Ok(retained)
236}
237
238fn reject_later_overlapping_write(
239 nodes: &[PlanNode],
240 producer_index: usize,
241 retained_binding: &ResolvedValueBinding,
242 retained_resource: &ResourceId,
243 retained_start: u64,
244 retained_end: u64,
245) -> Result<(), VNextError> {
246 for (node_index, node) in nodes.iter().enumerate().skip(producer_index) {
247 for binding in node.values() {
248 let is_target = node_index == producer_index
249 && binding.role() == retained_binding.role()
250 && binding.ordinal() == retained_binding.ordinal()
251 && binding.value_id() == retained_binding.value_id();
252 if is_target
253 || !matches!(
254 binding.access(),
255 TensorAccess::Write | TensorAccess::ReadWrite
256 )
257 {
258 continue;
259 }
260 for component in binding.storage().components() {
261 if component.resource_id() != retained_resource {
262 continue;
263 }
264 let write_end = component
265 .offset_bytes()
266 .checked_add(component.length_bytes())
267 .ok_or_else(|| invalid_plan("plan value write range overflows u64"))?;
268 if component.offset_bytes() < retained_end && retained_start < write_end {
269 return Err(invalid_plan(format!(
270 "completion retention value `{}` is overwritten by node `{}` after its producer",
271 retained_binding.value_id(),
272 node.id()
273 )));
274 }
275 }
276 }
277 }
278 Ok(())
279}