1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::{Arc, Mutex, RwLock};
3
4use thiserror::Error;
5use vyre_megakernel::{
6 Artifact, ArtifactEnvelope, CompileError, Diagnostic, TargetCompileError, TargetPayload,
7 TargetPayloadFormat, ValidatedCompileRequest,
8};
9
10use crate::pipeline_cache::{PipelineCacheStore, PipelineFingerprint};
11use vyre_driver::{
12 ArtifactInstance, ArtifactMaterializer, BackendError, BackendRegistration, BindingSet,
13 BoundResource, Completion, DeviceIdentity, Resource, Submission,
14};
15use vyre_megakernel::{AbiAccess, ArtifactValueId, Digest, ResourceLifetime};
16
17#[derive(Clone, Debug, PartialEq, Eq, Error)]
19#[error("artifact admission rejected: {source}")]
20pub struct ArtifactAdmissionError {
21 #[source]
22 source: CompileError,
23}
24
25impl ArtifactAdmissionError {
26 #[must_use]
28 pub const fn diagnostic(&self) -> &Diagnostic {
29 &self.source.diagnostic
30 }
31
32 #[must_use]
34 pub fn into_compile_error(self) -> CompileError {
35 self.source
36 }
37}
38
39impl From<CompileError> for ArtifactAdmissionError {
40 fn from(source: CompileError) -> Self {
41 Self { source }
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct AdmittedArtifact {
48 envelope: ArtifactEnvelope,
49 target_payload_index: usize,
50}
51
52impl AdmittedArtifact {
53 #[must_use]
55 pub const fn envelope(&self) -> &ArtifactEnvelope {
56 &self.envelope
57 }
58
59 #[must_use]
61 pub const fn neutral(&self) -> &Artifact {
62 self.envelope.neutral()
63 }
64
65 #[must_use]
67 pub fn target_payload(&self) -> &TargetPayload {
68 &self.envelope.target_payloads()[self.target_payload_index]
69 }
70
71 #[must_use]
73 pub fn into_envelope(self) -> ArtifactEnvelope {
74 self.envelope
75 }
76}
77
78pub fn admit_artifact(
80 envelope_bytes: &[u8],
81 required_format: &TargetPayloadFormat,
82) -> Result<AdmittedArtifact, ArtifactAdmissionError> {
83 let envelope = ArtifactEnvelope::from_bytes(envelope_bytes)?;
84 admit_envelope(envelope, required_format)
85}
86
87pub fn admit_envelope(
92 envelope: ArtifactEnvelope,
93 required_format: &TargetPayloadFormat,
94) -> Result<AdmittedArtifact, ArtifactAdmissionError> {
95 let target_payload_index = envelope.require_target_payload_index(required_format)?;
96 Ok(AdmittedArtifact {
97 envelope,
98 target_payload_index,
99 })
100}
101
102pub fn admit_cached_artifact(
115 store: &dyn PipelineCacheStore,
116 fingerprint: &PipelineFingerprint,
117 required_format: &TargetPayloadFormat,
118) -> Result<Option<AdmittedArtifact>, ArtifactAdmissionError> {
119 let Some(payload) = store.get(fingerprint) else {
120 return Ok(None);
121 };
122 admit_artifact(&payload, required_format).map(Some)
123}
124
125#[derive(Debug, Error)]
127pub enum ArtifactSessionError {
128 #[error(transparent)]
130 Admission(#[from] ArtifactAdmissionError),
131 #[error(transparent)]
133 Compile(#[from] CompileError),
134 #[error(transparent)]
136 Target(#[from] TargetCompileError),
137 #[error(transparent)]
139 Backend(#[from] BackendError),
140 #[error("artifact session state is poisoned: {0}. Fix: discard and rebuild the session")]
142 State(String),
143}
144
145struct MaterializedArtifact {
146 admitted: AdmittedArtifact,
147 materializer: Arc<dyn ArtifactMaterializer>,
148 instance: Box<dyn ArtifactInstance>,
149}
150
151pub struct ArtifactSession {
153 registration: &'static BackendRegistration,
154 state: RwLock<MaterializedArtifact>,
155}
156
157impl ArtifactSession {
158 pub fn compile(
161 registration: &'static BackendRegistration,
162 request: &ValidatedCompileRequest,
163 ) -> Result<Self, ArtifactSessionError> {
164 let materializer = Arc::from(registration.materializer()?);
165 Self::compile_with_materializer(registration, request, materializer)
166 }
167 pub fn compile_with_materializer(
169 registration: &'static BackendRegistration,
170 request: &ValidatedCompileRequest,
171 materializer: Arc<dyn ArtifactMaterializer>,
172 ) -> Result<Self, ArtifactSessionError> {
173 let artifact = vyre_megakernel::compile(request)?;
174 let compiler = registration.target_compiler()?;
175 let envelope = vyre_megakernel::attach_target(artifact, compiler.as_ref())?;
176 Self::from_envelope_with_materializer(registration, envelope, materializer)
177 }
178
179 pub fn from_envelope(
181 registration: &'static BackendRegistration,
182 envelope: ArtifactEnvelope,
183 ) -> Result<Self, ArtifactSessionError> {
184 let materializer = Arc::from(registration.materializer()?);
185 Self::from_envelope_with_materializer(registration, envelope, materializer)
186 }
187
188 pub fn from_envelope_with_materializer(
190 registration: &'static BackendRegistration,
191 envelope: ArtifactEnvelope,
192 materializer: Arc<dyn ArtifactMaterializer>,
193 ) -> Result<Self, ArtifactSessionError> {
194 let admitted = admit_envelope(envelope, materializer.device().target_format())?;
195 let instance = materializer.materialize(admitted.neutral(), admitted.target_payload())?;
196 validate_instance(&admitted, materializer.as_ref(), instance.as_ref())?;
197 Ok(Self {
198 registration,
199 state: RwLock::new(MaterializedArtifact {
200 admitted,
201 materializer,
202 instance,
203 }),
204 })
205 }
206
207 pub fn from_bytes(
209 registration: &'static BackendRegistration,
210 envelope_bytes: &[u8],
211 ) -> Result<Self, ArtifactSessionError> {
212 let envelope =
213 ArtifactEnvelope::from_bytes(envelope_bytes).map_err(ArtifactAdmissionError::from)?;
214 Self::from_envelope(registration, envelope)
215 }
216
217 pub fn artifact(&self) -> Result<Digest, ArtifactSessionError> {
219 let state = self
220 .state
221 .read()
222 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
223 Ok(state.admitted.neutral().digest())
224 }
225 pub fn payload(&self) -> Result<Digest, ArtifactSessionError> {
227 let state = self
228 .state
229 .read()
230 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
231 Ok(state.admitted.target_payload().digest())
232 }
233
234 pub fn device(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
236 let state = self
237 .state
238 .read()
239 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
240 Ok(state.instance.device().clone())
241 }
242
243 pub fn bindings(&self) -> Result<BindingSet, ArtifactSessionError> {
245 Ok(BindingSet::new(self.artifact()?))
246 }
247
248 pub fn submit(
250 &self,
251 bindings: BindingSet,
252 ) -> Result<Box<dyn Submission>, ArtifactSessionError> {
253 let state = self
254 .state
255 .read()
256 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
257 Ok(state.instance.submit(bindings)?)
258 }
259
260 pub fn submit_and_wait(
262 &self,
263 bindings: BindingSet,
264 ) -> Result<Completion, ArtifactSessionError> {
265 Ok(self.submit(bindings)?.wait()?)
266 }
267
268 pub fn rematerialize(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
272 let mut state = self
273 .state
274 .write()
275 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
276 let materializer: Arc<dyn ArtifactMaterializer> =
277 Arc::from(self.registration.materializer()?);
278 let admitted = admit_envelope(
279 state.admitted.envelope().clone(),
280 materializer.device().target_format(),
281 )?;
282 let instance = materializer.materialize(admitted.neutral(), admitted.target_payload())?;
283 validate_instance(&admitted, materializer.as_ref(), instance.as_ref())?;
284 let identity = instance.device().clone();
285 *state = MaterializedArtifact {
286 admitted,
287 materializer,
288 instance,
289 };
290 Ok(identity)
291 }
292
293 pub fn resource(&self, name: &str) -> Result<ArtifactValueId, ArtifactSessionError> {
295 let state = self
296 .state
297 .read()
298 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
299 state
300 .admitted
301 .neutral()
302 .resources()
303 .iter()
304 .find(|resource| resource.name == name)
305 .map(|resource| resource.value)
306 .ok_or_else(|| {
307 BackendError::InvalidProgram {
308 fix: format!(
309 "Fix: artifact ABI does not declare required runtime resource `{name}`."
310 ),
311 }
312 .into()
313 })
314 }
315 pub fn allocate_resident(&self, byte_len: usize) -> Result<Resource, ArtifactSessionError> {
317 let state = self
318 .state
319 .read()
320 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
321 Ok(state.materializer.allocate_resident(byte_len)?)
322 }
323
324 pub fn upload_resident(
326 &self,
327 resource: &Resource,
328 bytes: &[u8],
329 ) -> Result<(), ArtifactSessionError> {
330 let state = self
331 .state
332 .read()
333 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
334 Ok(state.materializer.upload_resident(resource, bytes)?)
335 }
336
337 pub fn upload_resident_at(
339 &self,
340 resource: &Resource,
341 offset_bytes: usize,
342 bytes: &[u8],
343 ) -> Result<(), ArtifactSessionError> {
344 let state = self
345 .state
346 .read()
347 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
348 Ok(state
349 .materializer
350 .upload_resident_at(resource, offset_bytes, bytes)?)
351 }
352
353 pub fn free_resident(&self, resource: Resource) -> Result<(), ArtifactSessionError> {
355 let state = self
356 .state
357 .read()
358 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
359 Ok(state.materializer.free_resident(resource)?)
360 }
361
362 pub fn resident_bindings(
364 &self,
365 resources: &[Resource],
366 ) -> Result<BindingSet, ArtifactSessionError> {
367 let state = self
368 .state
369 .read()
370 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
371 let entries = state.admitted.target_payload().entries();
372 if entries.len() != 1 {
373 return Err(BackendError::UnsupportedFeature {
374 name: "resident bindings for multi-entry artifacts".to_string(),
375 backend: state.instance.device().backend.to_string(),
376 }
377 .into());
378 }
379 let bindings = &entries[0].resource_bindings;
380 if bindings.len() != resources.len() {
381 return Err(BackendError::InvalidProgram {
382 fix: format!(
383 "Fix: target entry requires {} resident resource(s), but the caller supplied {}.",
384 bindings.len(),
385 resources.len()
386 ),
387 }
388 .into());
389 }
390 let mut typed = BindingSet::new(state.admitted.neutral().digest());
391 for (binding, resource) in bindings.into_iter().zip(resources) {
392 typed.insert(binding.resource, BoundResource::Resident(resource.clone()));
393 }
394 Ok(typed)
395 }
396
397 pub fn host_bindings(&self, inputs: &[&[u8]]) -> Result<BindingSet, ArtifactSessionError> {
399 let state = self
400 .state
401 .read()
402 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
403 let artifact = state.admitted.neutral();
404 let mut resources = artifact
405 .abi()
406 .resources
407 .iter()
408 .filter(|resource| match resource.access {
409 AbiAccess::ReadOnly | AbiAccess::Uniform => true,
410 AbiAccess::ReadWrite => artifact
411 .resources()
412 .iter()
413 .find(|record| record.value == resource.value)
414 .is_none_or(|record| record.lifetime != ResourceLifetime::Output),
415 AbiAccess::WriteOnly => false,
416 })
417 .collect::<Vec<_>>();
418 resources.sort_unstable_by_key(|resource| resource.slot);
419 if resources.len() != inputs.len() {
420 return Err(BackendError::InvalidProgram {
421 fix: format!(
422 "Fix: artifact ABI requires {} host input buffer(s), but the caller supplied {}.",
423 resources.len(),
424 inputs.len()
425 ),
426 }
427 .into());
428 }
429 let mut bindings = BindingSet::new(state.admitted.neutral().digest());
430 for (resource, bytes) in resources.into_iter().zip(inputs) {
431 bindings.insert(resource.value, BoundResource::Host(bytes.to_vec()));
432 }
433 Ok(bindings)
434 }
435
436 pub fn submit_host_inputs(&self, inputs: &[&[u8]]) -> Result<Completion, ArtifactSessionError> {
438 self.submit_and_wait(self.host_bindings(inputs)?)
439 }
440
441 pub fn ordered_outputs(
443 &self,
444 completion: &Completion,
445 ) -> Result<Vec<Vec<u8>>, ArtifactSessionError> {
446 let state = self
447 .state
448 .read()
449 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
450 let mut resources = state
451 .admitted
452 .neutral()
453 .abi()
454 .resources
455 .iter()
456 .filter(|resource| {
457 matches!(resource.access, AbiAccess::ReadWrite | AbiAccess::WriteOnly)
458 })
459 .collect::<Vec<_>>();
460 resources.sort_unstable_by_key(|resource| resource.slot);
461 resources
462 .into_iter()
463 .map(|resource| {
464 completion
465 .outputs
466 .get(&resource.value)
467 .or_else(|| completion.retained.get(&resource.value))
468 .cloned()
469 .ok_or_else(|| {
470 BackendError::InvalidProgram {
471 fix: format!(
472 "Fix: materializer completion must project writable artifact value {}.",
473 resource.value.0
474 ),
475 }
476 .into()
477 })
478 })
479 .collect()
480 }
481
482 fn retained_values(&self) -> Result<BTreeSet<ArtifactValueId>, ArtifactSessionError> {
483 let state = self
484 .state
485 .read()
486 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
487 Ok(state
488 .admitted
489 .neutral()
490 .resources()
491 .iter()
492 .filter(|resource| resource.lifetime == ResourceLifetime::Retained)
493 .map(|resource| resource.value)
494 .collect())
495 }
496}
497
498pub struct RetainedArtifactSession {
500 session: ArtifactSession,
501 retained_values: BTreeSet<ArtifactValueId>,
502 retained: Mutex<BTreeMap<ArtifactValueId, Vec<u8>>>,
503}
504
505impl RetainedArtifactSession {
506 pub fn new(
508 session: ArtifactSession,
509 initial: BTreeMap<ArtifactValueId, Vec<u8>>,
510 ) -> Result<Self, ArtifactSessionError> {
511 let retained_values = session.retained_values()?;
512 if initial.keys().copied().collect::<BTreeSet<_>>() != retained_values {
513 return Err(BackendError::InvalidProgram {
514 fix: "Fix: initialize exactly every retained artifact value before creating a retained session.".to_string(),
515 }
516 .into());
517 }
518 Ok(Self {
519 session,
520 retained_values,
521 retained: Mutex::new(initial),
522 })
523 }
524
525 pub fn artifact(&self) -> Result<Digest, ArtifactSessionError> {
527 self.session.artifact()
528 }
529
530 pub fn device(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
532 self.session.device()
533 }
534
535 pub fn bindings(&self) -> Result<BindingSet, ArtifactSessionError> {
537 self.session.bindings()
538 }
539
540 pub fn rematerialize(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
542 self.session.rematerialize()
543 }
544
545 pub fn replace_retained(
551 &self,
552 values: BTreeMap<ArtifactValueId, Vec<u8>>,
553 ) -> Result<(), ArtifactSessionError> {
554 if values.keys().copied().collect::<BTreeSet<_>>() != self.retained_values {
555 return Err(BackendError::InvalidProgram {
556 fix: "Fix: replace exactly every retained artifact value.".to_string(),
557 }
558 .into());
559 }
560 *self
561 .retained
562 .lock()
563 .map_err(|error| ArtifactSessionError::State(error.to_string()))? = values;
564 Ok(())
565 }
566
567 pub fn submit_and_wait(
569 &self,
570 mut bindings: BindingSet,
571 ) -> Result<Completion, ArtifactSessionError> {
572 if bindings.artifact() != self.session.artifact()? {
573 return Err(BackendError::InvalidProgram {
574 fix: "Fix: retained session bindings must name the session artifact digest."
575 .to_string(),
576 }
577 .into());
578 }
579 {
580 let retained = self
581 .retained
582 .lock()
583 .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
584 for (value, bytes) in retained.iter() {
585 bindings.insert(*value, BoundResource::Host(bytes.clone()));
586 }
587 }
588 let completion = self.session.submit_and_wait(bindings)?;
589 if completion.retained.keys().copied().collect::<BTreeSet<_>>() != self.retained_values {
590 return Err(BackendError::InvalidProgram {
591 fix: "Fix: artifact completion must return exactly every retained ABI value."
592 .to_string(),
593 }
594 .into());
595 }
596 *self
597 .retained
598 .lock()
599 .map_err(|error| ArtifactSessionError::State(error.to_string()))? =
600 completion.retained.clone();
601 Ok(completion)
602 }
603}
604
605fn validate_instance(
606 admitted: &AdmittedArtifact,
607 materializer: &dyn ArtifactMaterializer,
608 instance: &dyn ArtifactInstance,
609) -> Result<(), BackendError> {
610 if instance.artifact() != admitted.neutral().digest()
611 || instance.payload() != admitted.target_payload().digest()
612 || instance.device() != materializer.device().identity()
613 {
614 return Err(BackendError::InvalidProgram {
615 fix: "Fix: materialized instance identities must exactly match the admitted artifact, target payload, and acquired device generation.".to_string(),
616 });
617 }
618 Ok(())
619}