1use std::fmt;
4
5use query_planning::{
6 admission::ExecutionCapability,
7 error::{QueryError, QueryErrorCode, Result, bytes_with_gib},
8};
9
10const OPERATION: &str = "query_engine.gpu.resource_profile";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct DeviceCapability {
19 capability: ExecutionCapability,
20}
21
22impl DeviceCapability {
23 pub const fn new(name: &'static str) -> Self {
25 Self { capability: ExecutionCapability::new(name) }
26 }
27
28 const fn from_requirement(requirement: ExecutionCapability) -> Self {
29 Self { capability: requirement }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct DeviceCapabilities {
42 support: DeviceCapabilitySupport,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46enum DeviceCapabilitySupport {
47 All,
48 Exact(Vec<DeviceCapability>),
49}
50
51impl Default for DeviceCapabilities {
52 fn default() -> Self {
53 Self::all()
54 }
55}
56
57impl DeviceCapabilities {
58 pub(crate) const fn all() -> Self {
60 Self { support: DeviceCapabilitySupport::All }
61 }
62
63 pub fn only(capabilities: impl IntoIterator<Item = DeviceCapability>) -> Self {
69 let mut exact = Vec::new();
70 for capability in capabilities {
71 if !exact.contains(&capability) {
72 exact.push(capability);
73 }
74 }
75 Self { support: DeviceCapabilitySupport::Exact(exact) }
76 }
77
78 pub(crate) fn supports(&self, requirement: &ExecutionCapability) -> bool {
79 match &self.support {
80 DeviceCapabilitySupport::All => true,
81 DeviceCapabilitySupport::Exact(capabilities) => {
82 capabilities.contains(&DeviceCapability::from_requirement(*requirement))
83 },
84 }
85 }
86
87 pub(crate) fn supports_all(&self, requirements: &[ExecutionCapability]) -> bool {
88 requirements.iter().all(|requirement| self.supports(requirement))
89 }
90
91 pub(crate) const fn requires_explicit_attempt_requirement(&self) -> bool {
97 matches!(self.support, DeviceCapabilitySupport::Exact(_))
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct DeviceResourceProfile {
109 pub device_ordinal: usize,
111 pub device_capabilities: DeviceCapabilities,
113 pub(crate) configured_limit_bytes: u64,
114 pub(crate) headroom_bytes: u64,
115 pub(crate) backend_reserve_bytes: u64,
116 pub(crate) usable_capacity_bytes: u64,
118 pub managed_capacity_bytes: u64,
120 pub default_query_limit_bytes: u64,
122 pub max_active_attempts: usize,
125 pub root_lane_count: usize,
127 pub root_stream_count: usize,
129 pub source_work_per_attempt: usize,
131 pub(crate) aux_source_work_capacity: usize,
134 pub stream_pool_size: usize,
136 pub worker_job_capacity: usize,
138 pub cache_cap_bytes: u64,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
150pub(crate) struct DeviceResourceEnvelope {
151 pub(crate) device_ordinal: usize,
153 pub(crate) device_capabilities: DeviceCapabilities,
154 pub(crate) configured_limit_bytes: u64,
156 pub(crate) headroom_bytes: u64,
158 pub(crate) backend_reserve_bytes: u64,
160 pub(crate) default_query_limit_bytes: u64,
162 pub(crate) max_active_attempts: usize,
164 pub(crate) source_work_per_attempt: usize,
173 pub(crate) stream_pool_size: usize,
175 pub(crate) worker_job_capacity: usize,
177 pub(crate) cache_cap_bytes: u64,
179}
180
181impl DeviceResourceProfile {
182 pub fn query_share_bytes(&self) -> u64 {
187 self.managed_capacity_bytes.saturating_sub(self.cache_cap_bytes)
188 }
189
190 pub(crate) fn derive_default_query_limit_bytes(
197 device_ordinal: usize,
198 managed_capacity_bytes: u64,
199 cache_cap_bytes: u64,
200 max_active_attempts: usize,
201 ) -> Result<u64> {
202 if max_active_attempts == 0 {
203 return Err(
204 invalid(device_ordinal, "max_active_attempts", "must be greater than zero").with_fact("reason", "zero")
205 );
206 }
207 let query_share_bytes =
208 managed_capacity_bytes.checked_sub(cache_cap_bytes).filter(|share| *share > 0).ok_or_else(|| {
209 invalid(
210 device_ordinal,
211 "cache_cap_bytes",
212 format!(
213 "is {}, at or above the managed capacity of {}; the largest accepted value is {}",
214 bytes_with_gib(cache_cap_bytes),
215 bytes_with_gib(managed_capacity_bytes),
216 bytes_with_gib(managed_capacity_bytes.saturating_sub(1)),
217 ),
218 )
219 .with_fact("reason", "cache_cap_exceeds_managed_capacity")
220 .with_fact("cache_cap_bytes", cache_cap_bytes)
221 .with_fact("managed_capacity_bytes", managed_capacity_bytes)
222 })?;
223 let divisor = u64::try_from(max_active_attempts).map_err(|_| {
224 invalid(device_ordinal, "max_active_attempts", "parallelism target exceeds the capacity integer range")
225 .with_fact("reason", "max_active_attempts_overflow")
226 })?;
227 let derived = query_share_bytes.checked_div(divisor).filter(|cap| *cap > 0).ok_or_else(|| {
228 invalid(device_ordinal, "default_query_limit_bytes", "derived default query limit is zero")
229 .with_fact("reason", "derived_default_query_limit_is_zero")
230 .with_fact("query_share_bytes", query_share_bytes)
231 .with_fact("max_active_attempts", max_active_attempts)
232 })?;
233 Ok(derived)
234 }
235
236 pub(crate) fn resolve(envelope: DeviceResourceEnvelope) -> Result<Self> {
239 let DeviceResourceEnvelope {
240 device_ordinal,
241 device_capabilities,
242 configured_limit_bytes,
243 headroom_bytes,
244 backend_reserve_bytes,
245 default_query_limit_bytes,
246 max_active_attempts,
247 source_work_per_attempt,
248 stream_pool_size,
249 worker_job_capacity,
250 cache_cap_bytes,
251 } = envelope;
252 let usable_capacity_bytes = configured_limit_bytes.checked_sub(headroom_bytes).ok_or_else(|| {
253 invalid(
254 device_ordinal,
255 "headroom_bytes",
256 format!(
257 "is {}, above the configured limit of {}",
258 bytes_with_gib(headroom_bytes),
259 bytes_with_gib(configured_limit_bytes),
260 ),
261 )
262 .with_fact("reason", "configured_limit_below_headroom")
263 .with_fact("configured_limit_bytes", configured_limit_bytes)
264 .with_fact("headroom_bytes", headroom_bytes)
265 })?;
266 let managed_capacity_bytes = usable_capacity_bytes.checked_sub(backend_reserve_bytes).ok_or_else(|| {
267 invalid(
268 device_ordinal,
269 "backend_reserve_bytes",
270 format!(
271 "is {}, above the usable capacity of {}; the largest accepted value is {}",
272 bytes_with_gib(backend_reserve_bytes),
273 bytes_with_gib(usable_capacity_bytes),
274 bytes_with_gib(usable_capacity_bytes.saturating_sub(1)),
275 ),
276 )
277 .with_fact("reason", "backend_reserve_exceeds_usable_capacity")
278 .with_fact("usable_capacity_bytes", usable_capacity_bytes)
279 .with_fact("backend_reserve_bytes", backend_reserve_bytes)
280 })?;
281 if managed_capacity_bytes == 0 {
282 return Err(invalid(
283 device_ordinal,
284 "managed_capacity_bytes",
285 format!(
286 "is zero; backend_reserve_bytes of {} takes the whole usable capacity of {}",
287 bytes_with_gib(backend_reserve_bytes),
288 bytes_with_gib(usable_capacity_bytes),
289 ),
290 )
291 .with_fact("reason", "zero_managed_capacity"));
292 }
293 if cache_cap_bytes >= managed_capacity_bytes {
294 return Err(invalid(
295 device_ordinal,
296 "cache_cap_bytes",
297 format!(
298 "is {}, at or above the managed capacity of {}; the largest accepted value is {}",
299 bytes_with_gib(cache_cap_bytes),
300 bytes_with_gib(managed_capacity_bytes),
301 bytes_with_gib(managed_capacity_bytes - 1),
302 ),
303 )
304 .with_fact("reason", "cache_cap_exceeds_managed_capacity")
305 .with_fact("cache_cap_bytes", cache_cap_bytes)
306 .with_fact("managed_capacity_bytes", managed_capacity_bytes));
307 }
308 if max_active_attempts == 0 {
309 return Err(
310 invalid(device_ordinal, "max_active_attempts", "must be greater than zero").with_fact("reason", "zero")
311 );
312 }
313 if default_query_limit_bytes == 0 {
314 return Err(invalid(device_ordinal, "default_query_limit_bytes", "must be greater than zero")
315 .with_fact("reason", "zero"));
316 }
317 let query_share_bytes = managed_capacity_bytes.checked_sub(cache_cap_bytes).ok_or_else(|| {
318 invalid(device_ordinal, "cache_cap_bytes", "cache cap and query share overflow capacity arithmetic")
319 .with_fact("reason", "cache_cap_overflow")
320 })?;
321 if default_query_limit_bytes > query_share_bytes {
322 return Err(invalid(
323 device_ordinal,
324 "default_query_limit_bytes",
325 format!(
326 "is {}, above the query share of {} (managed capacity {} minus cache_cap_bytes {}); the largest accepted value is {}",
327 bytes_with_gib(default_query_limit_bytes),
328 bytes_with_gib(query_share_bytes),
329 bytes_with_gib(managed_capacity_bytes),
330 bytes_with_gib(cache_cap_bytes),
331 bytes_with_gib(query_share_bytes),
332 ),
333 )
334 .with_fact("reason", "default_query_limit_exceeds_managed_capacity")
335 .with_fact("default_query_limit_bytes", default_query_limit_bytes)
336 .with_fact("query_share_bytes", query_share_bytes)
337 .with_fact("managed_capacity_bytes", managed_capacity_bytes));
338 }
339 let source_work_per_attempt = source_work_per_attempt.max(1);
340 let aux_source_work_capacity = max_active_attempts.checked_mul(source_work_per_attempt).ok_or_else(|| {
341 invalid(device_ordinal, "source_work_per_attempt", "auxiliary source-work capacity overflowed")
342 .with_fact("reason", "aux_source_work_overflow")
343 })?;
344 let required_slots = max_active_attempts.checked_add(aux_source_work_capacity).ok_or_else(|| {
345 invalid(device_ordinal, "stream_pool_size", "required stream capacity overflowed")
346 .with_fact("reason", "required_slot_overflow")
347 })?;
348 for (field, actual) in [("stream_pool_size", stream_pool_size), ("worker_job_capacity", worker_job_capacity)] {
349 if actual < required_slots {
350 return Err(invalid(device_ordinal, field, "must cover root and auxiliary source work")
351 .with_fact("reason", "below_required_slot_capacity")
352 .with_fact("required_slot_capacity", required_slots)
353 .with_fact("actual_capacity", actual));
354 }
355 }
356 Ok(Self {
357 device_ordinal,
358 device_capabilities,
359 configured_limit_bytes,
360 headroom_bytes,
361 backend_reserve_bytes,
362 usable_capacity_bytes,
363 managed_capacity_bytes,
364 default_query_limit_bytes,
365 max_active_attempts,
366 root_lane_count: max_active_attempts,
367 root_stream_count: max_active_attempts,
368 source_work_per_attempt,
369 aux_source_work_capacity,
370 stream_pool_size,
371 worker_job_capacity,
372 cache_cap_bytes,
373 })
374 }
375}
376
377fn invalid(device_ordinal: usize, field: &'static str, message: impl fmt::Display) -> QueryError {
378 QueryError::new(
379 QueryErrorCode::InvalidConfiguration,
380 OPERATION,
381 format!("device {device_ordinal} {field} {message}"),
382 )
383 .with_fact("device_ordinal", device_ordinal)
384 .with_fact("field", field)
385}
386
387#[cfg(test)]
388mod tests {
389 use query_planning::admission::ExecutionCapability;
390
391 use super::*;
392
393 #[test]
394 fn resolves_heterogeneous_profiles_and_slot_math() {
395 let small = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
396 device_ordinal: 0,
397 configured_limit_bytes: 1_000,
398 headroom_bytes: 100,
399 backend_reserve_bytes: 100,
400 default_query_limit_bytes: 100,
401 max_active_attempts: 2,
402 source_work_per_attempt: 3,
403 stream_pool_size: 8,
404 worker_job_capacity: 8,
405 device_capabilities: DeviceCapabilities::default(),
406 cache_cap_bytes: 0,
407 }).unwrap();
408 let large = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
409 device_ordinal: 1,
410 configured_limit_bytes: 2_000,
411 headroom_bytes: 200,
412 backend_reserve_bytes: 100,
413 default_query_limit_bytes: 100,
414 max_active_attempts: 3,
415 source_work_per_attempt: 1,
416 stream_pool_size: 6,
417 worker_job_capacity: 6,
418 device_capabilities: DeviceCapabilities::default(),
419 cache_cap_bytes: 0,
420 })
421 .unwrap();
422 assert_eq!((small.usable_capacity_bytes, small.managed_capacity_bytes), (900, 800));
423 assert_eq!((small.root_lane_count, small.root_stream_count, small.aux_source_work_capacity), (2, 2, 6));
424 assert_eq!((large.usable_capacity_bytes, large.managed_capacity_bytes), (1_800, 1_700));
425 }
426
427 #[test]
428 fn rejects_invalid_capacity_and_query_limit_arithmetic() {
429 for (limit, headroom, reserve, reason) in [
430 (99, 100, 0, "configured_limit_below_headroom"),
431 (100, 0, 101, "backend_reserve_exceeds_usable_capacity"),
432 (100, 0, 100, "zero_managed_capacity"),
433 ] {
434 let error = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
435 device_ordinal: 7,
436 configured_limit_bytes: limit,
437 headroom_bytes: headroom,
438 backend_reserve_bytes: reserve,
439 default_query_limit_bytes: 0,
440 max_active_attempts: 1,
441 source_work_per_attempt: 1,
442 stream_pool_size: 2,
443 worker_job_capacity: 2,
444 device_capabilities: DeviceCapabilities::default(),
445 cache_cap_bytes: 0,
446 })
447 .unwrap_err();
448 assert_eq!(error.fact_value("device_ordinal"), Some("7"));
449 assert_eq!(error.fact_value("reason"), Some(reason));
450 }
451 let error = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
452 device_ordinal: 7,
453 configured_limit_bytes: u64::MAX - 1,
454 headroom_bytes: 0,
455 backend_reserve_bytes: 0,
456 default_query_limit_bytes: u64::MAX,
457 max_active_attempts: 2,
458 source_work_per_attempt: 1,
459 stream_pool_size: 4,
460 worker_job_capacity: 4,
461 device_capabilities: DeviceCapabilities::default(),
462 cache_cap_bytes: 0,
463 })
464 .unwrap_err();
465 assert_eq!(error.fact_value("reason"), Some("default_query_limit_exceeds_managed_capacity"));
466 }
467
468 #[test]
469 fn validates_cache_cap_against_managed_capacity() {
470 let profile = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
471 device_ordinal: 0,
472 configured_limit_bytes: 1_000,
473 headroom_bytes: 0,
474 backend_reserve_bytes: 0,
475 default_query_limit_bytes: 100,
476 max_active_attempts: 2,
477 source_work_per_attempt: 1,
478 stream_pool_size: 4,
479 worker_job_capacity: 4,
480 device_capabilities: DeviceCapabilities::default(),
481 cache_cap_bytes: 800,
482 })
483 .unwrap();
484 assert_eq!(profile.cache_cap_bytes, 800);
485 let error = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
486 device_ordinal: 0,
487 configured_limit_bytes: 1_000,
488 headroom_bytes: 0,
489 backend_reserve_bytes: 0,
490 default_query_limit_bytes: 100,
491 max_active_attempts: 2,
492 source_work_per_attempt: 1,
493 stream_pool_size: 4,
494 worker_job_capacity: 4,
495 device_capabilities: DeviceCapabilities::default(),
496 cache_cap_bytes: 1_001,
497 })
498 .unwrap_err();
499 assert_eq!(error.fact_value("reason"), Some("cache_cap_exceeds_managed_capacity"));
500 }
501
502 #[test]
503 fn rejects_zero_default_query_limit_for_an_otherwise_valid_profile() {
504 let error = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
505 device_ordinal: 7,
506 configured_limit_bytes: 100,
507 headroom_bytes: 0,
508 backend_reserve_bytes: 0,
509 default_query_limit_bytes: 0,
510 max_active_attempts: 1,
511 source_work_per_attempt: 1,
512 stream_pool_size: 2,
513 worker_job_capacity: 2,
514 device_capabilities: DeviceCapabilities::default(),
515 cache_cap_bytes: 0,
516 })
517 .unwrap_err();
518 assert_eq!(error.fact_value("field"), Some("default_query_limit_bytes"));
519 assert_eq!(error.fact_value("reason"), Some("zero"));
520 }
521
522 #[test]
523 fn rejects_query_limit_and_slot_capacity_below_requirements() {
524 let query_limit = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
525 device_ordinal: 3,
526 configured_limit_bytes: 100,
527 headroom_bytes: 0,
528 backend_reserve_bytes: 0,
529 default_query_limit_bytes: 101,
530 max_active_attempts: 2,
531 source_work_per_attempt: 1,
532 stream_pool_size: 4,
533 worker_job_capacity: 4,
534 device_capabilities: DeviceCapabilities::default(),
535 cache_cap_bytes: 0,
536 })
537 .unwrap_err();
538 assert_eq!(query_limit.fact_value("reason"), Some("default_query_limit_exceeds_managed_capacity"));
539 let slots = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
540 device_ordinal: 3,
541 configured_limit_bytes: 100,
542 headroom_bytes: 0,
543 backend_reserve_bytes: 0,
544 default_query_limit_bytes: 10,
545 max_active_attempts: 2,
546 source_work_per_attempt: 3,
547 stream_pool_size: 7,
548 worker_job_capacity: 7,
549 device_capabilities: DeviceCapabilities::default(),
550 cache_cap_bytes: 0,
551 })
552 .unwrap_err();
553 assert_eq!(slots.fact_value("reason"), Some("below_required_slot_capacity"));
554 }
555
556 #[test]
557 fn frozen_capability_metadata_requires_exact_matches() {
558 let required = ExecutionCapability::new("test_operator");
559 let other = ExecutionCapability::new("other_operator");
560 let restricted = DeviceCapabilities::only([DeviceCapability::new("test_operator")]);
561
562 assert!(restricted.supports(&required));
563 assert!(!restricted.supports(&other));
564 assert!(!DeviceCapabilities::only(std::iter::empty::<DeviceCapability>()).supports(&required));
565 }
566
567 #[test]
568 fn derives_default_query_limit_from_parallelism_target_and_cache_share() {
569 assert_eq!(DeviceResourceProfile::derive_default_query_limit_bytes(0, 24, 8, 2).unwrap(), 8);
572 assert_eq!(DeviceResourceProfile::derive_default_query_limit_bytes(0, 10, 0, 3).unwrap(), 3);
573 assert_eq!(DeviceResourceProfile::derive_default_query_limit_bytes(0, 1_000, 0, 1).unwrap(), 1_000);
574
575 let zero_share = DeviceResourceProfile::derive_default_query_limit_bytes(0, 10, 9, 3).unwrap_err();
576 assert_eq!(zero_share.fact_value("reason"), Some("derived_default_query_limit_is_zero"));
577
578 let no_share = DeviceResourceProfile::derive_default_query_limit_bytes(0, 8, 8, 2).unwrap_err();
579 assert_eq!(no_share.fact_value("reason"), Some("cache_cap_exceeds_managed_capacity"));
580
581 let no_parallelism = DeviceResourceProfile::derive_default_query_limit_bytes(0, 24, 8, 0).unwrap_err();
582 assert_eq!(no_parallelism.fact_value("reason"), Some("zero"));
583 }
584
585 #[test]
586 fn query_share_subtracts_the_cache_share() {
587 let envelope = |cache_cap_bytes, default_query_limit_bytes| DeviceResourceEnvelope {
588 device_ordinal: 0,
589 configured_limit_bytes: 1_000,
590 headroom_bytes: 0,
591 backend_reserve_bytes: 0,
592 default_query_limit_bytes,
593 max_active_attempts: 2,
594 source_work_per_attempt: 1,
595 stream_pool_size: 4,
596 worker_job_capacity: 4,
597 device_capabilities: DeviceCapabilities::default(),
598 cache_cap_bytes,
599 };
600 let profile = DeviceResourceProfile::resolve(envelope(300, 700)).unwrap();
601 assert_eq!(profile.query_share_bytes(), 700);
602
603 let beyond_share = DeviceResourceProfile::resolve(envelope(300, 701)).unwrap_err();
604 assert_eq!(beyond_share.fact_value("reason"), Some("default_query_limit_exceeds_managed_capacity"));
605
606 let no_share = DeviceResourceProfile::resolve(envelope(1_000, 1)).unwrap_err();
607 assert_eq!(no_share.fact_value("reason"), Some("cache_cap_exceeds_managed_capacity"));
608 }
609}