1use super::{
2 invalid_resource, AllocationKind, AllocationLifetime, Arc, BTreeMap, BufferUsage,
3 DynamicPoolDomainSpec, DynamicStorageView, ElementType, InvocationLivenessMode,
4 LaneStableArenaSlotIdentity, LogicalBackingSliceAuthority, NodeId,
5 PhysicalBackingClaimIdentity, PlanHash, PlanNode, ResourceId, Serialize,
6 SubmissionWaveDomainCapacityLayout, SubmissionWaveDomainLayout, VNextError,
7};
8use crate::vnext::ReusableExecutionBucketId;
9use sha2::{Digest, Sha256};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct ProgramBindingSlot {
13 node_index: usize,
14 node_id: NodeId,
15 resource_id: ResourceId,
16 physical_offset_bytes: u64,
17 capacity_size_bytes: u64,
18 alignment_bytes: u64,
19}
20
21impl ProgramBindingSlot {
22 pub const fn node_index(&self) -> usize {
23 self.node_index
24 }
25
26 pub fn node_id(&self) -> &NodeId {
27 &self.node_id
28 }
29
30 pub fn resource_id(&self) -> &ResourceId {
31 &self.resource_id
32 }
33
34 pub const fn physical_offset_bytes(&self) -> u64 {
35 self.physical_offset_bytes
36 }
37
38 pub const fn capacity_size_bytes(&self) -> u64 {
39 self.capacity_size_bytes
40 }
41
42 pub const fn alignment_bytes(&self) -> u64 {
43 self.alignment_bytes
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51pub struct ProgramBindingLayout {
52 reusable_execution_bucket_id: ReusableExecutionBucketId,
53 claim_identity: PhysicalBackingClaimIdentity,
54 physical_size_bytes: u64,
55 slots: Vec<ProgramBindingSlot>,
56 fingerprint: String,
57}
58
59impl ProgramBindingLayout {
60 pub fn reusable_execution_bucket_id(&self) -> &ReusableExecutionBucketId {
61 &self.reusable_execution_bucket_id
62 }
63
64 pub fn claim_identity(&self) -> &PhysicalBackingClaimIdentity {
65 &self.claim_identity
66 }
67
68 pub const fn physical_size_bytes(&self) -> u64 {
69 self.physical_size_bytes
70 }
71
72 pub fn slots(&self) -> &[ProgramBindingSlot] {
73 &self.slots
74 }
75
76 pub fn fingerprint(&self) -> &str {
77 &self.fingerprint
78 }
79
80 pub fn slot_for_node(&self, node_index: usize) -> Option<&ProgramBindingSlot> {
81 self.slots
82 .binary_search_by_key(&node_index, ProgramBindingSlot::node_index)
83 .ok()
84 .and_then(|index| self.slots.get(index))
85 }
86}
87
88#[derive(Debug)]
92pub struct ProgramBindingExecutionBinding {
93 plan_hash: PlanHash,
94 layout: Arc<ProgramBindingLayout>,
95 lane_slot_identity: LaneStableArenaSlotIdentity,
96}
97
98impl ProgramBindingExecutionBinding {
99 pub(super) fn bind(
100 plan_hash: PlanHash,
101 node_count: usize,
102 layout: Arc<ProgramBindingLayout>,
103 lane_slot_identity: LaneStableArenaSlotIdentity,
104 backing_slices: &[LogicalBackingSliceAuthority],
105 ) -> Result<Arc<Self>, VNextError> {
106 if node_count == 0
107 || layout.slots().is_empty()
108 || layout
109 .slots()
110 .iter()
111 .any(|slot| slot.node_index() >= node_count)
112 || lane_slot_identity.lifetime() != AllocationLifetime::Invocation
113 || layout.reusable_execution_bucket_id()
114 != lane_slot_identity.reusable_execution_bucket_id()
115 {
116 return Err(invalid_resource(
117 "program binding layout differs from its plan or lane slot",
118 ));
119 }
120 for slot in layout.slots() {
121 let evidence = backing_slices
122 .binary_search_by(|slice| slice.resource_id().cmp(slot.resource_id()))
123 .ok()
124 .and_then(|index| backing_slices.get(index))
125 .map(LogicalBackingSliceAuthority::evidence)
126 .ok_or_else(|| {
127 invalid_resource(
128 "program binding slot has no claimed logical backing projection",
129 )
130 })?;
131 if evidence.physical_claim_identity() != layout.claim_identity()
132 || evidence.reusable_execution_bucket_id()
133 != Some(layout.reusable_execution_bucket_id())
134 || evidence.physical_offset_bytes() != slot.physical_offset_bytes()
135 || evidence.capacity_size_bytes() != slot.capacity_size_bytes()
136 || evidence.physical_size_bytes() != layout.physical_size_bytes()
137 || evidence.alignment_bytes() != slot.alignment_bytes()
138 || evidence.usage() != BufferUsage::Binding
139 || evidence.element_type() != ElementType::U8
140 {
141 return Err(invalid_resource(
142 "program binding slot differs from its claimed physical projection",
143 ));
144 }
145 }
146 Ok(Arc::new(Self {
147 plan_hash,
148 layout,
149 lane_slot_identity,
150 }))
151 }
152
153 pub fn plan_hash(&self) -> &PlanHash {
154 &self.plan_hash
155 }
156
157 pub fn layout(&self) -> &ProgramBindingLayout {
158 &self.layout
159 }
160
161 pub fn lane_slot_identity(&self) -> &LaneStableArenaSlotIdentity {
162 &self.lane_slot_identity
163 }
164
165 pub(super) fn node(self: &Arc<Self>, node_index: usize) -> Option<ProgramBindingNodeBinding> {
166 self.layout
167 .slot_for_node(node_index)
168 .map(|_| ProgramBindingNodeBinding {
169 execution: Arc::clone(self),
170 node_index,
171 })
172 }
173}
174
175#[derive(Debug, Clone)]
178pub struct ProgramBindingNodeBinding {
179 execution: Arc<ProgramBindingExecutionBinding>,
180 node_index: usize,
181}
182
183impl ProgramBindingNodeBinding {
184 pub const fn node_index(&self) -> usize {
185 self.node_index
186 }
187
188 pub fn plan_hash(&self) -> &PlanHash {
189 self.execution.plan_hash()
190 }
191
192 pub fn layout(&self) -> &ProgramBindingLayout {
193 self.execution.layout()
194 }
195
196 pub fn slot(&self) -> &ProgramBindingSlot {
197 self.execution
198 .layout()
199 .slot_for_node(self.node_index)
200 .expect("program binding node handle is constructed from one compiled slot")
201 }
202
203 pub fn lane_slot_identity(&self) -> &LaneStableArenaSlotIdentity {
204 self.execution.lane_slot_identity()
205 }
206}
207
208pub(super) fn compile_program_binding_layouts(
209 domains: &[DynamicPoolDomainSpec],
210 nodes: &[PlanNode],
211 layouts: &[Option<SubmissionWaveDomainLayout>],
212 reusable_capacity_layouts: &BTreeMap<
213 ReusableExecutionBucketId,
214 Vec<Option<SubmissionWaveDomainCapacityLayout>>,
215 >,
216) -> Result<BTreeMap<ReusableExecutionBucketId, ProgramBindingLayout>, VNextError> {
217 if domains.len() != layouts.len()
218 || reusable_capacity_layouts
219 .values()
220 .any(|capacities| capacities.len() != domains.len())
221 {
222 return Err(invalid_resource(
223 "program binding compiler received inconsistent domain layouts",
224 ));
225 }
226
227 let binding_domains = domains
228 .iter()
229 .enumerate()
230 .filter(|(_, domain)| {
231 domain
232 .descriptors
233 .iter()
234 .any(|descriptor| matches!(descriptor.kind(), AllocationKind::Binding { .. }))
235 })
236 .collect::<Vec<_>>();
237 if binding_domains.is_empty() {
238 return Ok(BTreeMap::new());
239 }
240 let [(domain_index, domain)] = binding_domains.as_slice() else {
241 return Err(invalid_resource(
242 "one compiled program cannot span multiple binding domains",
243 ));
244 };
245 if domain.pool.compatibility().usage() != BufferUsage::Binding
246 || domain.pool.compatibility().element_type() != ElementType::U8
247 || domain.pool.compatibility().profile().view() != DynamicStorageView::Contiguous
248 || domain.pool.invocation_liveness_mode() != InvocationLivenessMode::ConservativeConcurrent
249 || domain.descriptors.iter().any(|descriptor| {
250 descriptor.lifetime() != AllocationLifetime::Invocation
251 || descriptor.usage() != BufferUsage::Binding
252 || descriptor.element_type() != ElementType::U8
253 || !matches!(descriptor.kind(), AllocationKind::Binding { .. })
254 })
255 {
256 return Err(invalid_resource(
257 "program bindings require one contiguous conservative U8 binding domain",
258 ));
259 }
260 let base_layout = layouts
261 .get(*domain_index)
262 .and_then(Option::as_ref)
263 .ok_or_else(|| invalid_resource("program binding domain has no submission-wave layout"))?;
264 if base_layout.projection_count != domain.descriptors.len()
265 || base_layout.claim_identity.resource_ids().len() != domain.descriptors.len()
266 {
267 return Err(invalid_resource(
268 "program binding domain layout does not cover every binding descriptor",
269 ));
270 }
271
272 reusable_capacity_layouts
273 .iter()
274 .map(|(bucket_id, capacity_layouts)| {
275 let capacity = capacity_layouts
276 .get(*domain_index)
277 .and_then(Option::as_ref)
278 .ok_or_else(|| {
279 invalid_resource(
280 "reusable program binding bucket has no physical capacity layout",
281 )
282 })?;
283 if capacity.projections.len() != domain.descriptors.len()
284 || capacity.physical_size_bytes == 0
285 {
286 return Err(invalid_resource(
287 "reusable program binding capacity layout is incomplete",
288 ));
289 }
290
291 let mut slots = domain
292 .descriptors
293 .iter()
294 .enumerate()
295 .map(|(projection_index, descriptor)| {
296 let AllocationKind::Binding { node_id } = descriptor.kind() else {
297 unreachable!("binding domain was validated above")
298 };
299 let node_index = nodes
300 .iter()
301 .position(|node| node.id() == node_id)
302 .ok_or_else(|| {
303 invalid_resource(
304 "program binding descriptor references a missing plan node",
305 )
306 })?;
307 let node = &nodes[node_index];
308 if node.binding_resource() != Some(descriptor.base_resource_id())
309 || !node.resources().contains(descriptor.base_resource_id())
310 {
311 return Err(invalid_resource(
312 "program binding node does not own its binding descriptor",
313 ));
314 }
315 let projection = &capacity.projections[projection_index];
316 Ok(ProgramBindingSlot {
317 node_index,
318 node_id: node_id.clone(),
319 resource_id: descriptor.base_resource_id().clone(),
320 physical_offset_bytes: projection.physical_offset_bytes,
321 capacity_size_bytes: projection.capacity_size_bytes,
322 alignment_bytes: descriptor.alignment_bytes(),
323 })
324 })
325 .collect::<Result<Vec<_>, VNextError>>()?;
326 slots.sort_by_key(ProgramBindingSlot::physical_offset_bytes);
327 let contiguous_end = slots.iter().try_fold(0_u64, |expected_offset, slot| {
328 if slot.physical_offset_bytes != expected_offset
329 || slot.capacity_size_bytes == 0
330 || slot.physical_offset_bytes % slot.alignment_bytes != 0
331 || slot.capacity_size_bytes % slot.alignment_bytes != 0
332 {
333 return Err(invalid_resource(
334 "program binding slots are empty, misaligned, or non-contiguous",
335 ));
336 }
337 expected_offset
338 .checked_add(slot.capacity_size_bytes)
339 .ok_or_else(|| invalid_resource("program binding arena size overflows u64"))
340 })?;
341 if contiguous_end != capacity.physical_size_bytes {
342 return Err(invalid_resource(
343 "program binding slots do not cover their physical arena exactly",
344 ));
345 }
346 slots.sort_by_key(ProgramBindingSlot::node_index);
347 if slots
348 .windows(2)
349 .any(|pair| pair[0].node_index >= pair[1].node_index)
350 {
351 return Err(invalid_resource(
352 "program binding slots do not have unique plan-node owners",
353 ));
354 }
355
356 #[derive(Serialize)]
357 struct FingerprintMaterial<'a> {
358 domain: &'static str,
359 reusable_execution_bucket_id: &'a ReusableExecutionBucketId,
360 claim_identity: &'a PhysicalBackingClaimIdentity,
361 physical_size_bytes: u64,
362 slots: &'a [ProgramBindingSlot],
363 }
364 let bytes = serde_json::to_vec(&FingerprintMaterial {
365 domain: "ferrum.runtime-vnext.program-binding-layout.v1",
366 reusable_execution_bucket_id: bucket_id,
367 claim_identity: &base_layout.claim_identity,
368 physical_size_bytes: capacity.physical_size_bytes,
369 slots: &slots,
370 })
371 .map_err(|error| {
372 invalid_resource(format!(
373 "program binding layout fingerprint encode failed: {error}"
374 ))
375 })?;
376 let layout = ProgramBindingLayout {
377 reusable_execution_bucket_id: bucket_id.clone(),
378 claim_identity: base_layout.claim_identity.clone(),
379 physical_size_bytes: capacity.physical_size_bytes,
380 slots,
381 fingerprint: format!("sha256/{:x}", Sha256::digest(bytes)),
382 };
383 Ok((bucket_id.clone(), layout))
384 })
385 .collect()
386}
387
388#[cfg(test)]
389mod tests {
390 use super::{
391 compile_program_binding_layouts, DynamicPoolDomainSpec, SubmissionWaveDomainCapacityLayout,
392 SubmissionWaveDomainLayout,
393 };
394 use crate::vnext::{
395 DynamicResourceDemand, DynamicResourceDescriptor, DynamicStorageAllocator,
396 DynamicStorageContract, DynamicStorageProfile, DynamicStorageView, MemoryPlan, NodeId,
397 PlanNode, ResolvedReusableExecutionBucket, ResourceId, ReusableExecutionBucketId,
398 ReusableExecutionBucketSpec, ReusableExecutionCapacity, ReusableExecutionClassId,
399 ReusableExecutionMemoryPlan, ReusablePoolWorkspaceBudget,
400 };
401 use std::collections::BTreeMap;
402
403 fn binding_descriptor(
404 resource_id: &str,
405 node_id: &str,
406 bytes: u64,
407 storage: DynamicStorageContract,
408 ) -> DynamicResourceDescriptor {
409 DynamicResourceDescriptor::resource_test_binding(
410 ResourceId::new(resource_id).expect("valid resource id"),
411 DynamicResourceDemand::fixed(bytes).expect("valid fixed binding demand"),
412 16,
413 NodeId::new(node_id).expect("valid node id"),
414 storage,
415 8,
416 )
417 .expect("valid binding descriptor")
418 }
419
420 fn compile_two_node_layout() -> (
421 Vec<PlanNode>,
422 Vec<DynamicPoolDomainSpec>,
423 Vec<Option<SubmissionWaveDomainLayout>>,
424 BTreeMap<ReusableExecutionBucketId, Vec<Option<SubmissionWaveDomainCapacityLayout>>>,
425 ReusableExecutionBucketId,
426 ) {
427 let storage = DynamicStorageContract::resource_test_contract(
428 DynamicStorageProfile::new(
429 DynamicStorageAllocator::LinearArena,
430 DynamicStorageView::Contiguous,
431 )
432 .expect("valid binding storage profile"),
433 "a".repeat(64),
434 )
435 .expect("valid binding storage");
436 let descriptors = vec![
437 binding_descriptor(
438 "resource/program-binding-first",
439 "node/program-binding-first",
440 64,
441 storage.clone(),
442 ),
443 binding_descriptor(
444 "resource/program-binding-second",
445 "node/program-binding-second",
446 128,
447 storage,
448 ),
449 ];
450 let nodes = vec![
451 PlanNode::resource_test_node_with_binding(
452 NodeId::new("node/program-binding-first").unwrap(),
453 descriptors[0].base_resource_id().clone(),
454 ),
455 PlanNode::resource_test_node_with_binding(
456 NodeId::new("node/program-binding-second").unwrap(),
457 descriptors[1].base_resource_id().clone(),
458 ),
459 ];
460 let pools =
461 MemoryPlan::derive_dynamic_pools(&descriptors, &nodes, 1 << 20).expect("derive pools");
462 let (_, domains) =
463 super::super::plan_dynamic_pool_admission(1, &pools, &descriptors).unwrap();
464 let layouts = domains
465 .iter()
466 .map(|domain| {
467 super::super::dynamic_pool::compile_submission_wave_domain_layout(domain, &nodes)
468 })
469 .collect::<Result<Vec<_>, _>>()
470 .unwrap();
471 let bucket = ReusableExecutionBucketSpec::new(
472 ReusableExecutionClassId::new("test.program-binding").unwrap(),
473 ReusableExecutionCapacity::new(1, 1, 1).unwrap(),
474 )
475 .unwrap();
476 let bucket_id = bucket.bucket_id().clone();
477 let reusable = ReusableExecutionMemoryPlan::new(
478 1,
479 1,
480 vec![ResolvedReusableExecutionBucket::new(
481 bucket,
482 vec![
483 ReusablePoolWorkspaceBudget::new(descriptors[0].pool_id().clone(), 0, 192)
484 .unwrap(),
485 ],
486 )
487 .unwrap()],
488 )
489 .unwrap();
490 let capacity_layouts =
491 super::super::dynamic_pool::compile_submission_wave_reusable_capacity_layouts(
492 &domains,
493 &layouts,
494 Some(&reusable),
495 )
496 .unwrap();
497 (nodes, domains, layouts, capacity_layouts, bucket_id)
498 }
499
500 #[test]
501 fn binding_layout_cold_compiles_one_exact_stable_arena() {
502 let (nodes, domains, layouts, capacity_layouts, bucket_id) = compile_two_node_layout();
503 let compiled =
504 compile_program_binding_layouts(&domains, &nodes, &layouts, &capacity_layouts)
505 .expect("compile binding layout");
506 let layout = compiled.get(&bucket_id).expect("compiled bucket layout");
507
508 assert_eq!(layout.physical_size_bytes(), 192);
509 assert_eq!(layout.slots().len(), 2);
510 assert_eq!(layout.slot_for_node(0).unwrap().physical_offset_bytes(), 0);
511 assert_eq!(layout.slot_for_node(0).unwrap().capacity_size_bytes(), 64);
512 assert_eq!(layout.slot_for_node(1).unwrap().physical_offset_bytes(), 64);
513 assert_eq!(layout.slot_for_node(1).unwrap().capacity_size_bytes(), 128);
514 assert!(layout.fingerprint().starts_with("sha256/"));
515 assert_eq!(
516 compiled,
517 compile_program_binding_layouts(&domains, &nodes, &layouts, &capacity_layouts)
518 .expect("recompile stable binding layout")
519 );
520 }
521
522 #[test]
523 fn binding_layout_rejects_a_node_that_does_not_own_its_binding() {
524 let (_, domains, layouts, capacity_layouts, _) = compile_two_node_layout();
525 let nodes = vec![
526 PlanNode::resource_test_node(NodeId::new("node/program-binding-first").unwrap()),
527 PlanNode::resource_test_node(NodeId::new("node/program-binding-second").unwrap()),
528 ];
529
530 let error = compile_program_binding_layouts(&domains, &nodes, &layouts, &capacity_layouts)
531 .expect_err("binding layout must reject missing node ownership");
532 assert!(error
533 .to_string()
534 .contains("does not own its binding descriptor"));
535 }
536}