Skip to main content

qvm_scheduler/scheduler/
multiplexing.rs

1//! Spatial and temporal multiplexing for quantum circuit scheduling
2
3use crate::{QvmError, Result, Topology};
4use crate::scheduler::{Job, Assignment, BinPacker, BinPackingAlgorithm};
5use crate::topology::{TileFinder, TilePreferences, TileManager};
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9/// Spatial multiplexing configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SpatialMultiplexConfig {
12    /// Enable spatial multiplexing
13    pub enabled: bool,
14    /// Maximum number of parallel jobs
15    pub max_parallel_jobs: usize,
16    /// Minimum distance between parallel jobs
17    pub min_job_separation: usize,
18    /// Buffer zone size for crosstalk mitigation
19    pub buffer_zone_size: usize,
20    /// Spatial isolation strategy
21    pub isolation_strategy: SpatialIsolationStrategy,
22}
23
24impl Default for SpatialMultiplexConfig {
25    fn default() -> Self {
26        Self {
27            enabled: true,
28            max_parallel_jobs: 4,
29            min_job_separation: 2,
30            buffer_zone_size: 1,
31            isolation_strategy: SpatialIsolationStrategy::TileBased,
32        }
33    }
34}
35
36/// Spatial isolation strategies
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum SpatialIsolationStrategy {
39    /// Tile-based isolation using non-overlapping tiles
40    TileBased,
41    /// Distance-based isolation using minimum qubit distances
42    DistanceBased,
43    /// Buffer zone isolation with dedicated buffer qubits
44    BufferZoned,
45    /// Connectivity-based isolation using graph partitioning
46    ConnectivityBased,
47}
48
49/// Temporal multiplexing configuration
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TemporalMultiplexConfig {
52    /// Enable temporal multiplexing
53    pub enabled: bool,
54    /// Minimum batch size for temporal scheduling
55    pub min_batch_size: usize,
56    /// Maximum batch size
57    pub max_batch_size: usize,
58    /// Time slice duration (microseconds)
59    pub time_slice_duration: u64,
60    /// Overlap tolerance between batches
61    pub overlap_tolerance: u64,
62    /// Temporal scheduling strategy
63    pub scheduling_strategy: TemporalSchedulingStrategy,
64}
65
66impl Default for TemporalMultiplexConfig {
67    fn default() -> Self {
68        Self {
69            enabled: true,
70            min_batch_size: 2,
71            max_batch_size: 10,
72            time_slice_duration: 100_000, // 100ms
73            overlap_tolerance: 5_000,     // 5ms
74            scheduling_strategy: TemporalSchedulingStrategy::RoundRobin,
75        }
76    }
77}
78
79/// Temporal scheduling strategies
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum TemporalSchedulingStrategy {
82    /// Round-robin scheduling across time slices
83    RoundRobin,
84    /// Priority-based temporal scheduling
85    PriorityBased,
86    /// Deadline-aware temporal scheduling
87    DeadlineAware,
88    /// Load-balancing temporal scheduling
89    LoadBalanced,
90}
91
92/// Spatial multiplexer for parallel job execution
93#[derive(Debug, Clone)]
94pub struct SpatialMultiplexer {
95    topology: Topology,
96    tile_manager: TileManager,
97    config: SpatialMultiplexConfig,
98}
99
100impl SpatialMultiplexer {
101    /// Create a new spatial multiplexer
102    pub fn new(topology: Topology, config: SpatialMultiplexConfig) -> Self {
103        let tile_manager = TileManager::new();
104        
105        Self {
106            topology,
107            tile_manager,
108            config,
109        }
110    }
111
112    /// Schedule jobs using spatial multiplexing
113    pub async fn schedule_spatially(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
114        if !self.config.enabled {
115            // Fall back to sequential scheduling
116            return self.schedule_sequentially(jobs).await;
117        }
118
119        // Group jobs that can be executed in parallel
120        let parallel_groups = self.create_spatial_groups(jobs)?;
121        let mut all_assignments = Vec::new();
122
123        for group in parallel_groups {
124            let assignments = self.schedule_parallel_group(group).await?;
125            all_assignments.extend(assignments);
126        }
127
128        Ok(all_assignments)
129    }
130
131    /// Create groups of jobs that can be executed spatially in parallel
132    fn create_spatial_groups(&self, jobs: Vec<Job>) -> Result<Vec<Vec<Job>>> {
133        let mut groups = Vec::new();
134        let mut remaining_jobs = jobs;
135
136        while !remaining_jobs.is_empty() {
137            let mut current_group = Vec::new();
138            let mut used_tiles = HashSet::new();
139            let mut i = 0;
140
141            // Greedy selection for current parallel group
142            while i < remaining_jobs.len() && current_group.len() < self.config.max_parallel_jobs {
143                let job = &remaining_jobs[i];
144                
145                if let Ok(tile) = self.find_suitable_tile_for_job(job, &used_tiles) {
146                    current_group.push(remaining_jobs.remove(i));
147                    used_tiles.insert(tile.id);
148                } else {
149                    i += 1;
150                }
151            }
152
153            // If no jobs could be added to current group, take the first remaining job
154            if current_group.is_empty() && !remaining_jobs.is_empty() {
155                current_group.push(remaining_jobs.remove(0));
156            }
157
158            if !current_group.is_empty() {
159                groups.push(current_group);
160            }
161        }
162
163        Ok(groups)
164    }
165
166    /// Schedule a group of jobs to execute in parallel
167    async fn schedule_parallel_group(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
168        match self.config.isolation_strategy {
169            SpatialIsolationStrategy::TileBased => {
170                self.schedule_tile_based(jobs).await
171            }
172            SpatialIsolationStrategy::DistanceBased => {
173                self.schedule_distance_based(jobs).await
174            }
175            SpatialIsolationStrategy::BufferZoned => {
176                self.schedule_buffer_zoned(jobs).await
177            }
178            SpatialIsolationStrategy::ConnectivityBased => {
179                self.schedule_connectivity_based(jobs).await
180            }
181        }
182    }
183
184    /// Tile-based parallel scheduling
185    async fn schedule_tile_based(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
186        let mut assignments = Vec::new();
187        let start_time = 0u64; // All jobs in group start simultaneously
188
189        for job in jobs {
190            let preferences = TilePreferences {
191                min_width: 2,
192                min_height: 2,
193                max_qubits: job.requirements.qubits_needed * 2,
194                buffer_size: self.config.buffer_zone_size,
195                prefer_center: false, // Prefer edges for better isolation
196                min_connectivity: 0.5,
197                ..Default::default()
198            };
199
200            let tile_finder = TileFinder::new(&self.topology);
201            let tile = tile_finder
202                .find_best_tile(job.requirements.qubits_needed, &preferences)?
203                .ok_or_else(|| QvmError::scheduling_error("No suitable tile found for spatial multiplexing"))?;
204
205            let qubit_mapping: Vec<usize> = tile.qubits
206                .iter()
207                .take(job.requirements.qubits_needed)
208                .map(|q| q.index())
209                .collect();
210
211            let assignment = Assignment {
212                job_id: job.id,
213                tile_id: tile.id,
214                start_time,
215                duration: job.estimated_duration,
216                qubit_mapping,
217                classical_mapping: (0..job.circuit.num_classical).collect(),
218                resource_allocation: Default::default(),
219            };
220
221            assignments.push(assignment);
222        }
223
224        Ok(assignments)
225    }
226
227    /// Distance-based parallel scheduling
228    async fn schedule_distance_based(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
229        let mut assignments = Vec::new();
230        let mut used_qubits = HashSet::new();
231        let start_time = 0u64;
232
233        for job in jobs {
234            // Find qubits that are far enough from already used qubits
235            let suitable_qubits = self.find_distant_qubits(&used_qubits, job.requirements.qubits_needed)?;
236            
237            if suitable_qubits.len() < job.requirements.qubits_needed {
238                return Err(QvmError::scheduling_error("Not enough distant qubits available"));
239            }
240
241            let qubit_mapping: Vec<usize> = suitable_qubits
242                .into_iter()
243                .take(job.requirements.qubits_needed)
244                .collect();
245
246            // Mark qubits as used (including buffer zone)
247            for &qubit_idx in &qubit_mapping {
248                let buffer_qubits = self.topology.qubits_within_distance(
249                    qubit_idx.into(), 
250                    self.config.min_job_separation as u32
251                );
252                for (buffer_qubit, _) in buffer_qubits {
253                    used_qubits.insert(buffer_qubit.index());
254                }
255            }
256
257            let assignment = Assignment {
258                job_id: job.id,
259                tile_id: 0, // No specific tile for distance-based
260                start_time,
261                duration: job.estimated_duration,
262                qubit_mapping,
263                classical_mapping: (0..job.circuit.num_classical).collect(),
264                resource_allocation: Default::default(),
265            };
266
267            assignments.push(assignment);
268        }
269
270        Ok(assignments)
271    }
272
273    /// Buffer-zoned parallel scheduling
274    async fn schedule_buffer_zoned(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
275        // Implement buffer zone scheduling with explicit buffer qubit allocation
276        let mut assignments = Vec::new();
277        let mut used_qubits = HashSet::new();
278        let start_time = 0u64;
279
280        for job in jobs {
281            // Find qubits with sufficient buffer zones
282            let (job_qubits, buffer_qubits) = self.find_qubits_with_buffers(
283                &used_qubits, 
284                job.requirements.qubits_needed
285            )?;
286
287            let assignment = Assignment {
288                job_id: job.id,
289                tile_id: 0,
290                start_time,
291                duration: job.estimated_duration,
292                qubit_mapping: job_qubits.clone(),
293                classical_mapping: (0..job.circuit.num_classical).collect(),
294                resource_allocation: crate::scheduler::ResourceAllocation {
295                    buffer_qubits,
296                    ..Default::default()
297                },
298            };
299
300            // Mark all qubits (job + buffer) as used
301            for &qubit in &assignment.qubit_mapping {
302                used_qubits.insert(qubit);
303            }
304            for &buffer in &assignment.resource_allocation.buffer_qubits {
305                used_qubits.insert(buffer);
306            }
307
308            assignments.push(assignment);
309        }
310
311        Ok(assignments)
312    }
313
314    /// Connectivity-based parallel scheduling
315    async fn schedule_connectivity_based(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
316        // Implement graph partitioning-based scheduling
317        let partitions = self.partition_topology(jobs.len())?;
318        let mut assignments = Vec::new();
319        let start_time = 0u64;
320
321        for (job, partition) in jobs.into_iter().zip(partitions) {
322            if partition.len() < job.requirements.qubits_needed {
323                return Err(QvmError::scheduling_error("Partition too small for job requirements"));
324            }
325
326            let qubit_mapping: Vec<usize> = partition
327                .into_iter()
328                .take(job.requirements.qubits_needed)
329                .collect();
330
331            let assignment = Assignment {
332                job_id: job.id,
333                tile_id: 0,
334                start_time,
335                duration: job.estimated_duration,
336                qubit_mapping,
337                classical_mapping: (0..job.circuit.num_classical).collect(),
338                resource_allocation: Default::default(),
339            };
340
341            assignments.push(assignment);
342        }
343
344        Ok(assignments)
345    }
346
347    /// Fall back to sequential scheduling
348    async fn schedule_sequentially(&self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
349        let bin_packer = BinPacker::with_algorithm(BinPackingAlgorithm::FirstFitDecreasing);
350        let batch = crate::scheduler::batch::Batch {
351            id: 0,
352            jobs,
353            metadata: Default::default(),
354        };
355        bin_packer.pack_batch(batch, &self.topology).await
356    }
357
358    /// Helper methods
359    fn find_suitable_tile_for_job(&self, job: &Job, used_tiles: &HashSet<usize>) -> Result<crate::topology::Tile> {
360        // Simplified tile finding - in practice, you'd need proper implementation
361        Err(QvmError::scheduling_error("Tile finding not implemented"))
362    }
363
364    fn find_distant_qubits(&self, used_qubits: &HashSet<usize>, needed: usize) -> Result<Vec<usize>> {
365        let mut suitable = Vec::new();
366        
367        for qubit in self.topology.qubits() {
368            let qubit_idx = qubit.index();
369            if used_qubits.contains(&qubit_idx) {
370                continue;
371            }
372
373            // Check if this qubit is far enough from all used qubits
374            let is_distant = used_qubits.iter().all(|&used_idx| {
375                if let Some(path) = self.topology.shortest_path(qubit, used_idx.into()) {
376                    path.len() > self.config.min_job_separation
377                } else {
378                    true // No path means they're isolated
379                }
380            });
381
382            if is_distant {
383                suitable.push(qubit_idx);
384                if suitable.len() >= needed {
385                    break;
386                }
387            }
388        }
389
390        Ok(suitable)
391    }
392
393    fn find_qubits_with_buffers(&self, used_qubits: &HashSet<usize>, needed: usize) -> Result<(Vec<usize>, Vec<usize>)> {
394        let mut job_qubits = Vec::new();
395        let mut buffer_qubits = Vec::new();
396        
397        for qubit in self.topology.qubits() {
398            let qubit_idx = qubit.index();
399            if used_qubits.contains(&qubit_idx) {
400                continue;
401            }
402
403            // Find buffer zone for this qubit
404            let buffer_zone = self.topology.qubits_within_distance(
405                qubit, 
406                self.config.buffer_zone_size as u32
407            );
408            
409            // Check if buffer zone is available
410            let buffer_available = buffer_zone.iter().all(|(buffer_qubit, _)| {
411                !used_qubits.contains(&buffer_qubit.index())
412            });
413
414            if buffer_available {
415                job_qubits.push(qubit_idx);
416                buffer_qubits.extend(
417                    buffer_zone.into_iter()
418                        .filter(|(q, _)| q.index() != qubit_idx)
419                        .map(|(q, _)| q.index())
420                );
421                
422                if job_qubits.len() >= needed {
423                    break;
424                }
425            }
426        }
427
428        Ok((job_qubits, buffer_qubits))
429    }
430
431    fn partition_topology(&self, num_partitions: usize) -> Result<Vec<Vec<usize>>> {
432        // Simplified graph partitioning - in practice, you'd use sophisticated algorithms
433        let qubits: Vec<_> = self.topology.qubits().into_iter().map(|q| q.index()).collect();
434        let partition_size = qubits.len() / num_partitions;
435        
436        let mut partitions = Vec::new();
437        for i in 0..num_partitions {
438            let start = i * partition_size;
439            let end = if i == num_partitions - 1 { qubits.len() } else { (i + 1) * partition_size };
440            partitions.push(qubits[start..end].to_vec());
441        }
442        
443        Ok(partitions)
444    }
445}
446
447/// Temporal multiplexer for sequential batch execution
448#[derive(Debug, Clone)]
449pub struct TemporalMultiplexer {
450    config: TemporalMultiplexConfig,
451}
452
453impl TemporalMultiplexer {
454    /// Create a new temporal multiplexer
455    pub fn new(config: TemporalMultiplexConfig) -> Self {
456        Self { config }
457    }
458
459    /// Schedule jobs using temporal multiplexing
460    pub async fn schedule_temporally(&self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
461        if !self.config.enabled {
462            return Ok(vec![]); // Delegate to main scheduler
463        }
464
465        // Create temporal batches
466        let batches = self.create_temporal_batches(jobs)?;
467        let mut all_assignments = Vec::new();
468        let mut current_time = 0u64;
469
470        for batch in batches {
471            let batch_assignments = self.schedule_temporal_batch(batch, current_time).await?;
472            
473            // Update current time for next batch
474            if let Some(max_end_time) = batch_assignments.iter()
475                .map(|a| a.start_time + a.duration)
476                .max() {
477                current_time = max_end_time + self.config.overlap_tolerance;
478            }
479
480            all_assignments.extend(batch_assignments);
481        }
482
483        Ok(all_assignments)
484    }
485
486    /// Create temporal batches based on configuration
487    fn create_temporal_batches(&self, mut jobs: Vec<Job>) -> Result<Vec<Vec<Job>>> {
488        match self.config.scheduling_strategy {
489            TemporalSchedulingStrategy::RoundRobin => {
490                self.create_round_robin_batches(jobs)
491            }
492            TemporalSchedulingStrategy::PriorityBased => {
493                jobs.sort_by_key(|job| std::cmp::Reverse(job.priority));
494                self.create_fixed_size_batches(jobs)
495            }
496            TemporalSchedulingStrategy::DeadlineAware => {
497                jobs.sort_by_key(|job| job.deadline.unwrap_or(u64::MAX));
498                self.create_fixed_size_batches(jobs)
499            }
500            TemporalSchedulingStrategy::LoadBalanced => {
501                self.create_load_balanced_batches(jobs)
502            }
503        }
504    }
505
506    fn create_round_robin_batches(&self, jobs: Vec<Job>) -> Result<Vec<Vec<Job>>> {
507        let mut batches = Vec::new();
508        let mut current_batch = Vec::new();
509
510        for job in jobs {
511            current_batch.push(job);
512            
513            if current_batch.len() >= self.config.max_batch_size {
514                batches.push(current_batch);
515                current_batch = Vec::new();
516            }
517        }
518
519        if !current_batch.is_empty() {
520            batches.push(current_batch);
521        }
522
523        Ok(batches)
524    }
525
526    fn create_fixed_size_batches(&self, jobs: Vec<Job>) -> Result<Vec<Vec<Job>>> {
527        let mut batches = Vec::new();
528        let chunk_size = self.config.max_batch_size;
529
530        for chunk in jobs.chunks(chunk_size) {
531            batches.push(chunk.to_vec());
532        }
533
534        Ok(batches)
535    }
536
537    fn create_load_balanced_batches(&self, jobs: Vec<Job>) -> Result<Vec<Vec<Job>>> {
538        // Balance batches by total execution time
539        let mut batches = Vec::new();
540        let mut current_batch = Vec::new();
541        let mut current_batch_duration = 0u64;
542
543        for job in jobs {
544            if current_batch_duration + job.estimated_duration > self.config.time_slice_duration 
545                && current_batch.len() >= self.config.min_batch_size {
546                batches.push(current_batch);
547                current_batch = Vec::new();
548                current_batch_duration = 0;
549            }
550
551            current_batch_duration += job.estimated_duration;
552            current_batch.push(job);
553
554            if current_batch.len() >= self.config.max_batch_size {
555                batches.push(current_batch);
556                current_batch = Vec::new();
557                current_batch_duration = 0;
558            }
559        }
560
561        if !current_batch.is_empty() {
562            batches.push(current_batch);
563        }
564
565        Ok(batches)
566    }
567
568    async fn schedule_temporal_batch(&self, jobs: Vec<Job>, start_time: u64) -> Result<Vec<Assignment>> {
569        // Create assignments for a temporal batch
570        // In practice, this would delegate to the main scheduler
571        let mut assignments = Vec::new();
572
573        for (i, job) in jobs.into_iter().enumerate() {
574            let assignment = Assignment {
575                job_id: job.id,
576                tile_id: 0,
577                start_time: start_time + (i as u64 * 1000), // Simple time offset
578                duration: job.estimated_duration,
579                qubit_mapping: (0..job.requirements.qubits_needed).collect(),
580                classical_mapping: (0..job.circuit.num_classical).collect(),
581                resource_allocation: Default::default(),
582            };
583            assignments.push(assignment);
584        }
585
586        Ok(assignments)
587    }
588}
589
590/// Combined spatial and temporal multiplexer
591#[derive(Debug, Clone)]
592pub struct HybridMultiplexer {
593    spatial: SpatialMultiplexer,
594    temporal: TemporalMultiplexer,
595}
596
597impl HybridMultiplexer {
598    /// Create a new hybrid multiplexer
599    pub fn new(
600        topology: Topology,
601        spatial_config: SpatialMultiplexConfig,
602        temporal_config: TemporalMultiplexConfig,
603    ) -> Self {
604        Self {
605            spatial: SpatialMultiplexer::new(topology, spatial_config),
606            temporal: TemporalMultiplexer::new(temporal_config),
607        }
608    }
609
610    /// Schedule jobs using both spatial and temporal multiplexing
611    pub async fn schedule_hybrid(&mut self, jobs: Vec<Job>) -> Result<Vec<Assignment>> {
612        // First apply temporal multiplexing to create time-ordered batches
613        let temporal_batches = self.temporal.create_temporal_batches(jobs)?;
614        let mut all_assignments = Vec::new();
615        let mut current_time = 0u64;
616
617        for batch in temporal_batches {
618            // Then apply spatial multiplexing within each temporal batch
619            let mut spatial_assignments = self.spatial.schedule_spatially(batch).await?;
620            
621            // Adjust timing for temporal ordering
622            for assignment in &mut spatial_assignments {
623                assignment.start_time += current_time;
624            }
625
626            // Update current time for next batch
627            if let Some(max_end_time) = spatial_assignments.iter()
628                .map(|a| a.start_time + a.duration)
629                .max() {
630                current_time = max_end_time + self.temporal.config.overlap_tolerance;
631            }
632
633            all_assignments.extend(spatial_assignments);
634        }
635
636        Ok(all_assignments)
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::topology::TopologyBuilder;
644    use crate::circuit_ir::CircuitBuilder;
645
646    #[test]
647    fn test_spatial_multiplex_config() {
648        let config = SpatialMultiplexConfig::default();
649        assert!(config.enabled);
650        assert_eq!(config.max_parallel_jobs, 4);
651        assert_eq!(config.isolation_strategy, SpatialIsolationStrategy::TileBased);
652    }
653
654    #[test]
655    fn test_temporal_multiplex_config() {
656        let config = TemporalMultiplexConfig::default();
657        assert!(config.enabled);
658        assert_eq!(config.min_batch_size, 2);
659        assert_eq!(config.scheduling_strategy, TemporalSchedulingStrategy::RoundRobin);
660    }
661
662    #[tokio::test]
663    async fn test_temporal_multiplexer() {
664        let config = TemporalMultiplexConfig::default();
665        let multiplexer = TemporalMultiplexer::new(config);
666
667        let circuit = CircuitBuilder::new("test", 2, 2).h(0).unwrap().build();
668        let jobs = vec![
669            Job::new(0, circuit.clone()),
670            Job::new(1, circuit.clone()),
671            Job::new(2, circuit),
672        ];
673
674        let assignments = multiplexer.schedule_temporally(jobs).await.unwrap();
675        assert_eq!(assignments.len(), 3);
676        
677        // Check that assignments are time-ordered
678        for i in 1..assignments.len() {
679            assert!(assignments[i].start_time >= assignments[i-1].start_time);
680        }
681    }
682}