cqlib_core/compile/resource/
manager.rs1use super::model::QubitOrigin;
15use super::{
16 AncillaRequirement, ResourceError, ResourceLease, ResourceLimits, ResourcePlan, ResourcePolicy,
17 ResourceRequest,
18};
19use crate::circuit::{Circuit, Qubit};
20use std::collections::{BTreeMap, BTreeSet};
21use std::sync::atomic::{AtomicU64, Ordering};
22
23static NEXT_MANAGER_ID: AtomicU64 = AtomicU64::new(0);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum ResourcePhase {
28 PreLayout,
30 PostLayout,
32}
33
34impl ResourcePhase {
35 const fn diagnostic_name(self) -> &'static str {
36 match self {
37 Self::PreLayout => "pre-layout",
38 Self::PostLayout => "post-layout",
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44struct LeaseRecord {
45 requirement: AncillaRequirement,
46 qubits: Vec<Qubit>,
47}
48
49#[derive(Debug)]
62pub struct ResourceManager {
63 id: u64,
64 phase: ResourcePhase,
65 resources: BTreeMap<Qubit, QubitOrigin>,
66 leased_qubits: BTreeSet<Qubit>,
67 active_leases: BTreeMap<u64, LeaseRecord>,
68 revision: u64,
69 next_lease_id: u64,
70 policy: ResourcePolicy,
71 limits: ResourceLimits,
72}
73
74impl ResourceManager {
75 pub fn from_circuit(
89 circuit: &Circuit,
90 policy: ResourcePolicy,
91 limits: ResourceLimits,
92 ) -> Result<Self, ResourceError> {
93 if let Some(limit) = limits.max_total_qubits
94 && circuit.num_qubits() > limit
95 {
96 return Err(ResourceError::CapacityExceeded {
97 limit,
98 requested_total: circuit.num_qubits(),
99 });
100 }
101
102 let resources = circuit
103 .qubits()
104 .into_iter()
105 .map(|qubit| (qubit, QubitOrigin::Input))
106 .collect();
107
108 Ok(Self {
109 id: next_manager_id()?,
110 phase: ResourcePhase::PreLayout,
111 resources,
112 leased_qubits: BTreeSet::new(),
113 active_leases: BTreeMap::new(),
114 revision: 0,
115 next_lease_id: 0,
116 policy,
117 limits,
118 })
119 }
120
121 pub fn preview(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
141 self.validate_request(request)?;
142
143 match request.requirement {
144 AncillaRequirement::CleanZero => self.preview_clean(request),
145 AncillaRequirement::Dirty => self.preview_dirty(request),
146 }
147 }
148
149 pub fn commit(
164 &mut self,
165 circuit: &mut Circuit,
166 plan: ResourcePlan,
167 ) -> Result<ResourceLease, ResourceError> {
168 if plan.manager_id() != self.id {
169 return Err(ResourceError::ForeignPlan);
170 }
171 if plan.revision() != self.revision {
174 return Err(ResourceError::StalePlan {
175 plan_revision: plan.revision(),
176 current_revision: self.revision,
177 });
178 }
179
180 self.verify_consistency(circuit)?;
181 self.validate_plan(&plan)?;
182
183 let next_revision = self.next_revision()?;
184 let lease_id = self.next_lease_id;
185 let next_lease_id =
186 self.next_lease_id
187 .checked_add(1)
188 .ok_or(ResourceError::CounterOverflow {
189 counter: "lease identifier",
190 })?;
191
192 if !plan.new_qubits().is_empty() {
193 circuit
194 .add_qubits(plan.new_qubits().to_vec())
195 .map_err(|error| ResourceError::CircuitResourceMismatch {
196 reason: format!("circuit rejected validated ancillary qubits: {error}"),
197 })?;
198 }
199
200 for &qubit in plan.new_qubits() {
201 let previous = self.resources.insert(qubit, QubitOrigin::CompilerAllocated);
202 debug_assert!(previous.is_none());
203 }
204 for &qubit in plan.qubits() {
205 let inserted = self.leased_qubits.insert(qubit);
206 debug_assert!(inserted);
207 }
208
209 let lease = ResourceLease::new(
210 self.id,
211 lease_id,
212 plan.requirement(),
213 plan.qubits().to_vec(),
214 );
215 let previous = self.active_leases.insert(
216 lease_id,
217 LeaseRecord {
218 requirement: lease.requirement(),
219 qubits: lease.qubits().to_vec(),
220 },
221 );
222 debug_assert!(previous.is_none());
223
224 self.revision = next_revision;
225 self.next_lease_id = next_lease_id;
226 Ok(lease)
227 }
228
229 pub fn release(&mut self, lease: &ResourceLease) -> Result<(), ResourceError> {
244 if lease.manager_id() != self.id {
245 return Err(ResourceError::UnknownLease {
246 lease_id: lease.id(),
247 });
248 }
249 let Some(record) = self.active_leases.get(&lease.id()) else {
250 return Err(ResourceError::UnknownLease {
251 lease_id: lease.id(),
252 });
253 };
254 if record.requirement != lease.requirement() || record.qubits.as_slice() != lease.qubits() {
255 return Err(ResourceError::UnknownLease {
256 lease_id: lease.id(),
257 });
258 }
259 if record
260 .qubits
261 .iter()
262 .any(|qubit| !self.leased_qubits.contains(qubit))
263 {
264 return Err(ResourceError::CircuitResourceMismatch {
265 reason: format!("lease {} is missing a leased-qubit record", lease.id()),
266 });
267 }
268
269 let next_revision = self.next_revision()?;
270 let record = self
271 .active_leases
272 .remove(&lease.id())
273 .ok_or(ResourceError::UnknownLease {
274 lease_id: lease.id(),
275 })?;
276 for qubit in record.qubits {
277 let removed = self.leased_qubits.remove(&qubit);
278 debug_assert!(removed);
279 }
280 self.revision = next_revision;
281 Ok(())
282 }
283
284 pub fn enter_post_layout(&mut self, circuit: &Circuit) -> Result<(), ResourceError> {
297 if self.phase != ResourcePhase::PreLayout {
298 return Err(ResourceError::InvalidPhase {
299 operation: "enter_post_layout",
300 expected: ResourcePhase::PreLayout.diagnostic_name(),
301 actual: self.phase.diagnostic_name(),
302 });
303 }
304 self.verify_idle(circuit)?;
305 let next_revision = self.next_revision()?;
306 self.phase = ResourcePhase::PostLayout;
307 self.revision = next_revision;
308 Ok(())
309 }
310
311 pub fn verify_consistency(&self, circuit: &Circuit) -> Result<(), ResourceError> {
322 let circuit_qubits: BTreeSet<_> = circuit.qubits().into_iter().collect();
323 for qubit in &circuit_qubits {
324 if !self.resources.contains_key(qubit) {
325 return Err(ResourceError::CircuitResourceMismatch {
326 reason: format!("circuit qubit {qubit} has no resource record"),
327 });
328 }
329 }
330 for qubit in self.resources.keys() {
331 if !circuit_qubits.contains(qubit) {
332 return Err(ResourceError::CircuitResourceMismatch {
333 reason: format!("resource qubit {qubit} is absent from the circuit"),
334 });
335 }
336 }
337 for qubit in &self.leased_qubits {
338 if !self.resources.contains_key(qubit) {
339 return Err(ResourceError::CircuitResourceMismatch {
340 reason: format!("leased qubit {qubit} has no resource record"),
341 });
342 }
343 }
344
345 let mut indexed_lease_qubits = BTreeSet::new();
346 for (&lease_id, record) in &self.active_leases {
347 for &qubit in &record.qubits {
348 if !self.resources.contains_key(&qubit) {
349 return Err(ResourceError::CircuitResourceMismatch {
350 reason: format!("lease {lease_id} contains unknown qubit {qubit}"),
351 });
352 }
353 if !indexed_lease_qubits.insert(qubit) {
354 return Err(ResourceError::CircuitResourceMismatch {
355 reason: format!("qubit {qubit} is present in overlapping leases"),
356 });
357 }
358 }
359 }
360 if indexed_lease_qubits != self.leased_qubits {
361 return Err(ResourceError::CircuitResourceMismatch {
362 reason: "leased-qubit index does not match active leases".to_string(),
363 });
364 }
365 Ok(())
366 }
367
368 pub fn verify_idle(&self, circuit: &Circuit) -> Result<(), ResourceError> {
380 self.verify_consistency(circuit)?;
381 if !self.active_leases.is_empty() {
382 return Err(ResourceError::ActiveLeaseRemaining {
383 count: self.active_leases.len(),
384 });
385 }
386 Ok(())
387 }
388
389 fn preview_clean(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
390 let mut qubits = Vec::with_capacity(request.count);
391 self.select_origin(QubitOrigin::CompilerAllocated, request, &mut qubits);
392
393 let mut new_qubits = Vec::new();
394 if qubits.len() < request.count {
395 if self.phase == ResourcePhase::PostLayout {
396 return Err(self.insufficient(request, qubits.len()));
399 }
400
401 let required_new = request.count - qubits.len();
402 let allocated = self.num_compiler_allocated();
403 let available_new = self
404 .policy
405 .max_pre_layout_clean_ancillas
406 .saturating_sub(allocated);
407 if required_new > available_new {
408 return Err(self.insufficient(request, qubits.len() + available_new));
409 }
410 self.ensure_total_capacity(required_new)?;
411
412 new_qubits = self.allocate_new_qubits(required_new)?;
415 qubits.extend_from_slice(&new_qubits);
416 }
417
418 Ok(ResourcePlan::new(
419 self.id,
420 request.requirement,
421 qubits,
422 new_qubits,
423 self.revision,
424 ))
425 }
426
427 fn preview_dirty(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
428 let mut qubits = Vec::with_capacity(request.count);
429 self.select_origin(QubitOrigin::CompilerAllocated, request, &mut qubits);
430 if self.policy.allow_dirty_borrowing {
431 self.select_origin(QubitOrigin::Input, request, &mut qubits);
432 }
433 if qubits.len() < request.count {
434 return Err(self.insufficient(request, qubits.len()));
435 }
436
437 Ok(ResourcePlan::new(
438 self.id,
439 request.requirement,
440 qubits,
441 Vec::new(),
442 self.revision,
443 ))
444 }
445
446 fn select_origin(
447 &self,
448 origin: QubitOrigin,
449 request: &ResourceRequest,
450 selected: &mut Vec<Qubit>,
451 ) {
452 for (&qubit, &qubit_origin) in &self.resources {
453 if selected.len() == request.count {
454 break;
455 }
456 if qubit_origin == origin
457 && !self.leased_qubits.contains(&qubit)
458 && !request.excluded.contains(&qubit)
459 {
460 selected.push(qubit);
461 }
462 }
463 }
464
465 fn validate_request(&self, request: &ResourceRequest) -> Result<(), ResourceError> {
466 if request.count == 0 {
467 return Err(ResourceError::InvalidRequest {
468 reason: "ancillary-qubit count must be greater than zero".to_string(),
469 });
470 }
471 for qubit in &request.excluded {
472 if !self.resources.contains_key(qubit) {
473 return Err(ResourceError::InvalidRequest {
474 reason: format!("excluded qubit {qubit} is not registered"),
475 });
476 }
477 }
478 Ok(())
479 }
480
481 fn validate_plan(&self, plan: &ResourcePlan) -> Result<(), ResourceError> {
482 if !plan.new_qubits().is_empty() && self.phase != ResourcePhase::PreLayout {
483 return Err(ResourceError::InvalidPhase {
484 operation: "commit new logical ancillary qubits",
485 expected: ResourcePhase::PreLayout.diagnostic_name(),
486 actual: self.phase.diagnostic_name(),
487 });
488 }
489
490 let new_qubits: BTreeSet<_> = plan.new_qubits().iter().copied().collect();
491 if new_qubits.len() != plan.new_qubits().len() {
492 return Err(ResourceError::CircuitResourceMismatch {
493 reason: "resource plan contains duplicate new qubits".to_string(),
494 });
495 }
496 let selected_qubits: BTreeSet<_> = plan.qubits().iter().copied().collect();
497 if selected_qubits.len() != plan.qubits().len() {
498 return Err(ResourceError::CircuitResourceMismatch {
499 reason: "resource plan contains duplicate selected qubits".to_string(),
500 });
501 }
502 for qubit in &new_qubits {
503 if self.resources.contains_key(qubit) {
504 return Err(ResourceError::DuplicateQubit { qubit: *qubit });
505 }
506 }
507 for qubit in &selected_qubits {
508 if self.leased_qubits.contains(qubit) {
509 return Err(ResourceError::CircuitResourceMismatch {
510 reason: format!("resource plan selected already-leased qubit {qubit}"),
511 });
512 }
513 if !self.resources.contains_key(qubit) && !new_qubits.contains(qubit) {
514 return Err(ResourceError::CircuitResourceMismatch {
515 reason: format!("resource plan selected unknown qubit {qubit}"),
516 });
517 }
518 }
519 self.ensure_total_capacity(new_qubits.len())
520 }
521
522 fn allocate_new_qubits(&self, count: usize) -> Result<Vec<Qubit>, ResourceError> {
523 let mut qubits = Vec::with_capacity(count);
524 let mut next_id = match self.resources.last_key_value() {
525 Some((qubit, _)) => qubit
526 .id()
527 .checked_add(1)
528 .ok_or(ResourceError::QubitIdOverflow)?,
529 None => 0,
530 };
531 for index in 0..count {
532 qubits.push(Qubit::new(next_id));
533 if index + 1 < count {
534 next_id = next_id
535 .checked_add(1)
536 .ok_or(ResourceError::QubitIdOverflow)?;
537 }
538 }
539 Ok(qubits)
540 }
541
542 fn ensure_total_capacity(&self, additional: usize) -> Result<(), ResourceError> {
543 let requested_total = self
544 .resources
545 .len()
546 .checked_add(additional)
547 .ok_or_else(|| ResourceError::InvalidRequest {
548 reason: "logical-qubit count overflow".to_string(),
549 })?;
550 if let Some(limit) = self.limits.max_total_qubits
551 && requested_total > limit
552 {
553 return Err(ResourceError::CapacityExceeded {
554 limit,
555 requested_total,
556 });
557 }
558 Ok(())
559 }
560
561 fn num_compiler_allocated(&self) -> usize {
562 self.resources
563 .values()
564 .filter(|&&origin| origin == QubitOrigin::CompilerAllocated)
565 .count()
566 }
567
568 fn next_revision(&self) -> Result<u64, ResourceError> {
569 self.revision
570 .checked_add(1)
571 .ok_or(ResourceError::CounterOverflow {
572 counter: "revision",
573 })
574 }
575
576 fn insufficient(&self, request: &ResourceRequest, available: usize) -> ResourceError {
577 ResourceError::InsufficientResources {
578 requirement: request.requirement.diagnostic_name(),
579 requested: request.count,
580 available,
581 }
582 }
583}
584
585fn next_manager_id() -> Result<u64, ResourceError> {
586 NEXT_MANAGER_ID
587 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
588 .map_err(|_| ResourceError::CounterOverflow {
589 counter: "manager identifier",
590 })
591}