1use std::num::NonZeroUsize;
4
5use eredu_checkpoint::{AffineQuantization, WeightQuantization};
6use eredu_core::{
7 CompletionCancellationMode, DraftingPlan, ParallelRankTopology, PreparationPolicy,
8 QuantizationRequest, ResidencyRequest, SessionCapabilities,
9};
10
11use crate::{
12 CacheResidencyPolicy, CommunicationCompletionPolicy, LayerWeightResidency,
13 PipelineWireContract, WeightResidency,
14};
15
16#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
18pub enum DraftingLoadRequest {
19 #[default]
21 ArchitectureDefault,
22 Disabled,
24 Embedded {
26 max_draft_tokens: NonZeroUsize,
28 },
29 ExternalTarget,
31}
32
33impl DraftingLoadRequest {
34 pub fn embedded(max_draft_tokens: usize) -> Result<Self, NormalizedLoadRequestError> {
36 let max_draft_tokens = NonZeroUsize::new(max_draft_tokens)
37 .ok_or(NormalizedLoadRequestError::ZeroEmbeddedDraftCapacity)?;
38 Ok(Self::Embedded { max_draft_tokens })
39 }
40
41 pub const fn embedded_capacity(self) -> Option<NonZeroUsize> {
43 match self {
44 Self::Embedded { max_draft_tokens } => Some(max_draft_tokens),
45 Self::ArchitectureDefault | Self::Disabled | Self::ExternalTarget => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, Eq, PartialEq)]
52pub struct ParallelLoadRequest {
53 rank: ParallelRankTopology,
54 wire: PipelineWireContract,
55 maximum_batch_size: i32,
56 maximum_sequence_length: i32,
57 completion: CommunicationCompletionPolicy,
58}
59
60impl ParallelLoadRequest {
61 pub fn new(
63 rank: ParallelRankTopology,
64 wire: PipelineWireContract,
65 maximum_batch_size: i32,
66 maximum_sequence_length: i32,
67 completion: CommunicationCompletionPolicy,
68 ) -> Result<Self, NormalizedLoadRequestError> {
69 if rank.is_replicated() {
70 return Err(NormalizedLoadRequestError::ReplicatedParallelTopology);
71 }
72 if maximum_batch_size <= 0 || maximum_sequence_length <= 0 {
73 return Err(NormalizedLoadRequestError::InvalidInvocationLimits {
74 maximum_batch_size,
75 maximum_sequence_length,
76 });
77 }
78 Ok(Self {
79 rank,
80 wire,
81 maximum_batch_size,
82 maximum_sequence_length,
83 completion,
84 })
85 }
86
87 pub const fn rank(self) -> ParallelRankTopology {
89 self.rank
90 }
91
92 pub const fn wire(self) -> PipelineWireContract {
94 self.wire
95 }
96
97 pub const fn invocation_limits(self) -> (i32, i32) {
99 (self.maximum_batch_size, self.maximum_sequence_length)
100 }
101
102 pub const fn completion(self) -> CommunicationCompletionPolicy {
104 self.completion
105 }
106
107 const fn with_completion(mut self, completion: CommunicationCompletionPolicy) -> Self {
108 self.completion = completion;
109 self
110 }
111}
112
113#[derive(Debug, thiserror::Error)]
115#[non_exhaustive]
116pub enum NormalizedLoadRequestError {
117 #[error("source reader-cache limit must be positive")]
119 ZeroCachedShards,
120 #[error(
122 "partitioned invocation limits must be positive, got batch {maximum_batch_size} and sequence {maximum_sequence_length}"
123 )]
124 InvalidInvocationLimits {
125 maximum_batch_size: i32,
127 maximum_sequence_length: i32,
129 },
130 #[error("explicit parallel execution requires a non-replicated topology")]
132 ReplicatedParallelTopology,
133 #[error("parallel execution cannot replace an existing local completion policy")]
135 ConflictingCompletionPolicy,
136 #[error("a communication completion policy requires a parallel topology")]
138 OrphanedModelCompletion,
139 #[error("embedded draft capacity must be positive")]
141 ZeroEmbeddedDraftCapacity,
142 #[error("unsupported speculative drafting plan")]
144 UnsupportedDraftingPlan,
145 #[error("{0}")]
147 Completion(String),
148 #[error("{0}")]
150 Quantization(String),
151}
152
153#[derive(Debug, Clone, Default, Eq, PartialEq)]
159pub struct NormalizedLoadRequest {
160 quantization: Option<QuantizationRequest>,
161 max_cached_shards: Option<NonZeroUsize>,
162 parallel: Option<ParallelLoadRequest>,
163 communication_completion: Option<CommunicationCompletionPolicy>,
164 weight_residency: WeightResidency,
165 state_residency: CacheResidencyPolicy,
166 required_session_capabilities: SessionCapabilities,
167 prompt_cache_persistence: bool,
168 drafting: DraftingLoadRequest,
169}
170
171#[derive(Debug, Clone, Copy)]
173pub struct ValidatedModelLoadRequest<'a> {
174 request: &'a NormalizedLoadRequest,
175 policy: PreparationPolicy,
176}
177
178impl<'a> ValidatedModelLoadRequest<'a> {
179 pub const fn preparation_policy(self) -> PreparationPolicy {
181 self.policy
182 }
183
184 pub const fn request(self) -> &'a NormalizedLoadRequest {
186 self.request
187 }
188}
189
190impl NormalizedLoadRequest {
191 pub const fn with_prompt_cache_persistence(mut self, required: bool) -> Self {
193 self.prompt_cache_persistence = required;
194 self
195 }
196
197 pub const fn prompt_cache_persistence(&self) -> bool {
199 self.prompt_cache_persistence
200 }
201
202 pub const fn with_max_cached_shards(mut self, maximum: NonZeroUsize) -> Self {
204 self.max_cached_shards = Some(maximum);
205 self
206 }
207
208 pub const fn max_cached_shards(&self) -> usize {
210 match self.max_cached_shards {
211 Some(maximum) => maximum.get(),
212 None => self.weight_residency.max_cached_shards(),
213 }
214 }
215
216 pub fn with_quantization(quantization: QuantizationRequest) -> Self {
218 Self {
219 quantization: Some(quantization),
220 ..Self::default()
221 }
222 }
223
224 pub fn with_parallel_execution(
226 mut self,
227 parallel: ParallelLoadRequest,
228 ) -> Result<Self, NormalizedLoadRequestError> {
229 if self.communication_completion.is_some() {
230 return Err(NormalizedLoadRequestError::ConflictingCompletionPolicy);
231 }
232 self.parallel = Some(parallel);
233 Ok(self)
234 }
235
236 pub const fn with_communication_completion_policy(
238 mut self,
239 policy: CommunicationCompletionPolicy,
240 ) -> Self {
241 self.set_communication_completion_policy(policy);
242 self
243 }
244
245 pub const fn set_communication_completion_policy(
247 &mut self,
248 policy: CommunicationCompletionPolicy,
249 ) {
250 match self.parallel {
251 Some(parallel) => self.parallel = Some(parallel.with_completion(policy)),
252 None => self.communication_completion = Some(policy),
253 }
254 }
255
256 pub fn with_weight_residency(mut self, residency: WeightResidency) -> Self {
258 self.weight_residency = residency;
259 self
260 }
261
262 pub fn with_state_residency(mut self, residency: CacheResidencyPolicy) -> Self {
264 self.state_residency = residency;
265 self
266 }
267
268 pub const fn with_required_session_capabilities(
270 mut self,
271 capabilities: SessionCapabilities,
272 ) -> Self {
273 self.required_session_capabilities = capabilities;
274 self
275 }
276
277 pub const fn with_drafting(mut self, drafting: DraftingLoadRequest) -> Self {
279 self.drafting = drafting;
280 self
281 }
282
283 pub fn with_drafting_plan(
285 self,
286 plan: &DraftingPlan,
287 ) -> Result<Self, NormalizedLoadRequestError> {
288 let drafting = match plan {
289 DraftingPlan::Disabled => DraftingLoadRequest::Disabled,
290 DraftingPlan::Embedded {
291 max_draft_tokens, ..
292 } => DraftingLoadRequest::embedded(*max_draft_tokens)?,
293 DraftingPlan::External { .. } => DraftingLoadRequest::ExternalTarget,
294 _ => return Err(NormalizedLoadRequestError::UnsupportedDraftingPlan),
295 };
296 Ok(self.with_drafting(drafting))
297 }
298
299 pub const fn quantization(&self) -> Option<QuantizationRequest> {
301 self.quantization
302 }
303
304 pub const fn parallel_execution(&self) -> Option<ParallelLoadRequest> {
306 self.parallel
307 }
308
309 pub const fn parallel_topology(&self) -> Option<ParallelRankTopology> {
311 match self.parallel {
312 Some(parallel) => Some(parallel.rank()),
313 None => None,
314 }
315 }
316
317 pub const fn pipeline_wire_contract(&self) -> Option<PipelineWireContract> {
319 match self.parallel {
320 Some(parallel) => Some(parallel.wire()),
321 None => None,
322 }
323 }
324
325 pub const fn has_parallel_execution(&self) -> bool {
327 self.parallel.is_some()
328 }
329
330 pub fn partitioned_invocation_limits(&self) -> Option<(i32, i32)> {
332 self.parallel.map(ParallelLoadRequest::invocation_limits)
333 }
334
335 pub fn communication_completion_policy(
337 &self,
338 ) -> Result<Option<CommunicationCompletionPolicy>, NormalizedLoadRequestError> {
339 match (self.parallel, self.communication_completion) {
340 (Some(parallel), None) => Ok(Some(parallel.completion())),
341 (None, None) => Ok(None),
342 (None, Some(_)) => Err(NormalizedLoadRequestError::OrphanedModelCompletion),
343 (Some(_), Some(_)) => Err(NormalizedLoadRequestError::ConflictingCompletionPolicy),
344 }
345 }
346
347 pub fn realtime_completion_policy(
349 &self,
350 ) -> Result<CommunicationCompletionPolicy, NormalizedLoadRequestError> {
351 if let Some(parallel) = self.parallel {
352 return Ok(parallel.completion());
353 }
354 self.communication_completion.map_or_else(
355 || {
356 CommunicationCompletionPolicy::new(
357 std::time::Duration::from_secs(30),
358 CompletionCancellationMode::QuarantineUntilComplete,
359 )
360 .map_err(|error| NormalizedLoadRequestError::Completion(error.to_string()))
361 },
362 Ok,
363 )
364 }
365
366 pub const fn weight_residency(&self) -> WeightResidency {
368 self.weight_residency
369 }
370
371 pub const fn state_residency(&self) -> &CacheResidencyPolicy {
373 &self.state_residency
374 }
375
376 pub const fn required_session_capabilities(&self) -> SessionCapabilities {
378 self.required_session_capabilities
379 }
380
381 pub const fn drafting(&self) -> DraftingLoadRequest {
383 self.drafting
384 }
385
386 pub fn weight_quantization(
388 &self,
389 ) -> Result<Option<WeightQuantization>, NormalizedLoadRequestError> {
390 self.quantization
391 .map(|request| match request {
392 QuantizationRequest::Affine { group_size, bits } => {
393 let group_size = i32::try_from(group_size).map_err(|_| {
394 NormalizedLoadRequestError::Quantization(format!(
395 "group_size must fit in i32, got {group_size}"
396 ))
397 })?;
398 AffineQuantization::new(group_size, i32::from(bits))
399 .map(WeightQuantization::Affine)
400 .map_err(|error| {
401 NormalizedLoadRequestError::Quantization(error.to_string())
402 })
403 }
404 QuantizationRequest::MxFp4 => Ok(WeightQuantization::MxFp4),
405 _ => Err(NormalizedLoadRequestError::Quantization(
406 "unknown load-time transformation request".into(),
407 )),
408 })
409 .transpose()
410 }
411
412 pub fn validate_model_preparation(
414 &self,
415 ) -> Result<ValidatedModelLoadRequest<'_>, NormalizedLoadRequestError> {
416 if self.max_cached_shards() == 0 {
417 return Err(NormalizedLoadRequestError::ZeroCachedShards);
418 }
419 self.weight_quantization()?;
420 self.communication_completion_policy()?;
421 Ok(ValidatedModelLoadRequest {
422 request: self,
423 policy: self.project_preparation_policy(),
424 })
425 }
426
427 pub fn preparation_policy(&self) -> Result<PreparationPolicy, NormalizedLoadRequestError> {
429 self.validate_model_preparation()
430 .map(ValidatedModelLoadRequest::preparation_policy)
431 }
432
433 fn project_preparation_policy(&self) -> PreparationPolicy {
434 let residency = if self.weight_residency.parameter_bank_cache().is_some() {
435 ResidencyRequest::AddressableParameterBanks
436 } else {
437 match self.weight_residency.layers() {
438 LayerWeightResidency::FullyResident => ResidencyRequest::FullyResident,
439 LayerWeightResidency::LayerwiseHost(_) => ResidencyRequest::LayerwiseHost,
440 LayerWeightResidency::DenseDiskStream(_) => ResidencyRequest::DenseDiskStream,
441 }
442 };
443 let mut policy = PreparationPolicy::new(self.quantization, residency)
444 .with_required_session_capabilities(self.required_session_capabilities);
445 if let Some(topology) = self.parallel_topology() {
446 policy = policy.with_topology(topology.topology());
447 }
448 policy
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use eredu_core::{ParallelTopology, QuantizationRequest};
456
457 fn completion() -> CommunicationCompletionPolicy {
458 CommunicationCompletionPolicy::new(
459 std::time::Duration::from_secs(1),
460 CompletionCancellationMode::QuarantineUntilComplete,
461 )
462 .unwrap()
463 }
464
465 #[test]
466 fn parallel_policy_is_atomic_and_exact() {
467 let rank =
468 ParallelRankTopology::new(ParallelTopology::new(2, 1, 1, 1).unwrap(), 1).unwrap();
469 let parallel = ParallelLoadRequest::new(
470 rank,
471 PipelineWireContract::new(crate::PipelineActivationDtype::Float32),
472 2,
473 128,
474 completion(),
475 )
476 .unwrap();
477 let request = NormalizedLoadRequest::with_quantization(QuantizationRequest::MxFp4)
478 .with_parallel_execution(parallel)
479 .unwrap()
480 .with_required_session_capabilities(SessionCapabilities::new(true, false, true));
481
482 request.validate_model_preparation().unwrap();
483 assert_eq!(request.parallel_execution(), Some(parallel));
484 assert_eq!(request.parallel_topology(), Some(rank));
485 assert_eq!(request.partitioned_invocation_limits(), Some((2, 128)));
486 assert_eq!(
487 request.communication_completion_policy().unwrap(),
488 Some(completion())
489 );
490 assert_eq!(
491 request.preparation_policy().unwrap().topology(),
492 Some(rank.topology())
493 );
494 }
495
496 #[test]
497 fn invalid_parallel_geometry_fails_at_parallel_construction() {
498 let rank =
499 ParallelRankTopology::new(ParallelTopology::new(2, 1, 1, 1).unwrap(), 0).unwrap();
500 let request = ParallelLoadRequest::new(
501 rank,
502 PipelineWireContract::new(crate::PipelineActivationDtype::Float32),
503 0,
504 128,
505 completion(),
506 );
507 assert!(matches!(
508 request,
509 Err(NormalizedLoadRequestError::InvalidInvocationLimits { .. })
510 ));
511
512 let request = ParallelLoadRequest::new(
513 rank,
514 PipelineWireContract::new(crate::PipelineActivationDtype::Float32),
515 1,
516 -1,
517 completion(),
518 );
519 assert!(matches!(
520 request,
521 Err(NormalizedLoadRequestError::InvalidInvocationLimits { .. })
522 ));
523 }
524
525 #[test]
526 fn replicated_topology_is_rejected_at_parallel_construction() {
527 let rank =
528 ParallelRankTopology::new(ParallelTopology::new(1, 1, 1, 1).unwrap(), 0).unwrap();
529 assert!(matches!(
530 ParallelLoadRequest::new(
531 rank,
532 PipelineWireContract::new(crate::PipelineActivationDtype::Float32),
533 1,
534 128,
535 completion(),
536 ),
537 Err(NormalizedLoadRequestError::ReplicatedParallelTopology)
538 ));
539 }
540
541 #[test]
542 fn local_completion_cannot_be_reinterpreted_as_parallel_completion() {
543 let rank =
544 ParallelRankTopology::new(ParallelTopology::new(2, 1, 1, 1).unwrap(), 0).unwrap();
545 let parallel = ParallelLoadRequest::new(
546 rank,
547 PipelineWireContract::new(crate::PipelineActivationDtype::Float32),
548 1,
549 128,
550 completion(),
551 )
552 .unwrap();
553 let request =
554 NormalizedLoadRequest::default().with_communication_completion_policy(completion());
555 assert!(matches!(
556 request.with_parallel_execution(parallel),
557 Err(NormalizedLoadRequestError::ConflictingCompletionPolicy)
558 ));
559 }
560
561 #[test]
562 fn embedded_drafting_capacity_is_positive_by_construction() {
563 assert!(matches!(
564 DraftingLoadRequest::embedded(0),
565 Err(NormalizedLoadRequestError::ZeroEmbeddedDraftCapacity)
566 ));
567 assert_eq!(
568 DraftingLoadRequest::embedded(4)
569 .unwrap()
570 .embedded_capacity()
571 .unwrap()
572 .get(),
573 4
574 );
575 }
576
577 #[test]
578 fn local_realtime_completion_does_not_become_valid_model_communication() {
579 let request =
580 NormalizedLoadRequest::default().with_communication_completion_policy(completion());
581 assert_eq!(request.realtime_completion_policy().unwrap(), completion());
582 assert!(matches!(
583 request.validate_model_preparation(),
584 Err(NormalizedLoadRequestError::OrphanedModelCompletion)
585 ));
586 }
587}