1use crate::loom::{Compiler, CompilerOptions, Kernels, Specialization};
37use crate::{
38 Access, Buffer, Constants, Device, Error, GraphExec, Kernel, Result, Stream, Target,
39 benchmark::Distribution,
40};
41use std::{
42 collections::{BTreeMap, BTreeSet, HashMap},
43 sync::atomic::{AtomicU64, Ordering},
44 time::Instant,
45};
46
47#[path = "model/definition.rs"]
48mod definition;
49pub use definition::{ModelDefinition, ModelFragment};
50
51static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub struct Region {
59 session: u64,
60 allocation: usize,
61 offset: usize,
62 bytes: usize,
63}
64
65impl Region {
66 #[must_use]
68 pub fn len(self) -> usize {
69 self.bytes
70 }
71
72 #[must_use]
74 pub fn is_empty(self) -> bool {
75 self.bytes == 0
76 }
77
78 #[must_use]
80 pub fn offset(self) -> usize {
81 self.offset
82 }
83
84 pub fn slice(self, offset: usize, bytes: usize) -> Result<Self> {
86 let end = offset
87 .checked_add(bytes)
88 .ok_or_else(|| Error::Message("model region size overflow".into()))?;
89 if end > self.bytes {
90 return Err(Error::Message("model region exceeds its parent".into()));
91 }
92 Ok(Self {
93 offset: self
94 .offset
95 .checked_add(offset)
96 .ok_or_else(|| Error::Message("model region offset overflow".into()))?,
97 bytes,
98 ..self
99 })
100 }
101
102 #[must_use]
104 pub fn read(self) -> Binding {
105 Binding {
106 region: self,
107 access: Access::Read,
108 }
109 }
110
111 #[must_use]
113 pub fn write(self) -> Binding {
114 Binding {
115 region: self,
116 access: Access::Write,
117 }
118 }
119
120 #[must_use]
122 pub fn read_write(self) -> Binding {
123 Binding {
124 region: self,
125 access: Access::ReadWrite,
126 }
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub struct Binding {
133 pub region: Region,
135 pub access: Access,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141pub struct KernelId {
142 session: u64,
143 index: usize,
144}
145
146#[derive(Clone, Debug)]
148pub enum Arguments {
149 Indices(Vec<u32>),
151 Packed(Box<Constants>),
153}
154
155impl Arguments {
156 pub fn indices(values: impl IntoIterator<Item = u32>) -> Self {
158 Self::Indices(values.into_iter().collect())
159 }
160
161 #[must_use]
163 pub fn none() -> Self {
164 Self::Packed(Box::new(Constants::new()))
165 }
166
167 #[must_use]
169 pub fn packed(constants: Constants) -> Self {
170 Self::Packed(Box::new(constants))
171 }
172}
173
174#[derive(Clone, Debug)]
176pub struct Dispatch {
177 pub kernel: KernelId,
179 pub arguments: Arguments,
181 pub grid: [u32; 3],
183 pub bindings: Vec<Binding>,
185}
186
187impl Dispatch {
188 pub fn indices(
190 kernel: KernelId,
191 values: impl IntoIterator<Item = u32>,
192 grid: [u32; 3],
193 bindings: Vec<Binding>,
194 ) -> Self {
195 Self {
196 kernel,
197 arguments: Arguments::indices(values),
198 grid,
199 bindings,
200 }
201 }
202}
203
204#[derive(Clone, Debug)]
206pub enum Command {
207 Fill {
209 region: Region,
211 value: u8,
213 },
214 Dispatch(Dispatch),
216}
217
218#[derive(Clone)]
219enum PreparedCommand {
220 Fill {
221 region: Region,
222 value: u8,
223 },
224 Dispatch {
225 kernel: KernelId,
226 constants: Box<Constants>,
227 grid: [u32; 3],
228 bindings: Vec<Binding>,
229 },
230}
231
232struct PreparedGraph {
233 executable: GraphExec,
234 commands: Vec<PreparedCommand>,
235}
236
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238enum Placement {
239 Device,
240 Shared,
241}
242
243struct Allocation {
244 buffer: Buffer,
245 placement: Placement,
246 immutable: bool,
247}
248
249pub struct ModelSession {
255 id: u64,
256 stream: Stream,
257 allocations: Vec<Allocation>,
258 kernels: Vec<Kernel>,
259 cache: Kernels,
260 graphs: HashMap<usize, PreparedGraph>,
261 readback: Option<Buffer>,
262 failed: bool,
263}
264
265impl ModelSession {
266 pub fn in_context(context: &crate::inference::ModelContext) -> Result<Self> {
270 let mut stream = Device::open(context.runtime().gpu()?.index())?.stream()?;
271 if let Some(budget) = context.runtime().memory_budget() {
272 stream = stream.with_memory_budget(budget.clone());
273 }
274 Self::with_stream(stream, context.compiler()?)
275 }
276
277 pub fn open(index: i32) -> Result<Self> {
279 let device = Device::open(index)?;
280 let stream = device.stream()?;
281 let options = CompilerOptions::for_stream(&stream);
282 let compiler = Compiler::shared(None, options)?;
283 Self::with_stream(stream, compiler)
284 }
285
286 pub fn open_for(index: i32, expected_target: &str) -> Result<Self> {
288 let device = Device::open_for(index, expected_target)?;
289 let stream = device.stream()?;
290 let compiler = Compiler::for_stream(None, &stream)?;
291 Self::with_stream(stream, compiler)
292 }
293
294 pub fn with_compiler(device: Device, compiler: Compiler) -> Result<Self> {
299 if compiler.target() != device.target() {
300 return Err(Error::Message(format!(
301 "compiler target {} does not match device target {}",
302 compiler.target().as_str(),
303 device.target().as_str()
304 )));
305 }
306 Self::with_stream(device.stream()?, compiler)
307 }
308
309 fn with_stream(stream: Stream, compiler: Compiler) -> Result<Self> {
310 if compiler.target() != stream.target() {
311 return Err(Error::Message(format!(
312 "compiler target {} does not match device target {}",
313 compiler.target().as_str(),
314 stream.target().as_str()
315 )));
316 }
317 Ok(Self {
318 id: NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
319 stream,
320 allocations: Vec::new(),
321 kernels: Vec::new(),
322 cache: Kernels::new(compiler),
323 graphs: HashMap::new(),
324 readback: None,
325 failed: false,
326 })
327 }
328
329 #[must_use]
331 pub fn target(&self) -> &Target {
332 self.stream.target()
333 }
334
335 pub fn allocate(&mut self, bytes: usize) -> Result<Region> {
337 self.allocate_with(bytes, Placement::Device)
338 }
339
340 pub fn allocate_shared(&mut self, bytes: usize) -> Result<Region> {
342 self.allocate_with(bytes, Placement::Shared)
343 }
344
345 fn allocate_with(&mut self, bytes: usize, placement: Placement) -> Result<Region> {
346 self.usable()?;
347 let buffer = match placement {
348 Placement::Device => self.stream.allocate(bytes)?,
349 Placement::Shared => self.stream.allocate_shared(bytes)?,
350 };
351 let allocation = self.allocations.len();
352 self.allocations.push(Allocation {
353 buffer,
354 placement,
355 immutable: false,
356 });
357 Ok(Region {
358 session: self.id,
359 allocation,
360 offset: 0,
361 bytes,
362 })
363 }
364
365 pub fn weight(&mut self, bytes: &[u8]) -> Result<Region> {
367 let region = self.allocate(bytes.len())?;
368 self.upload(region, bytes)?;
369 self.allocations[region.allocation].immutable = true;
370 Ok(region)
371 }
372
373 pub unsafe fn compile(
381 &mut self,
382 specifications: &[(&str, Specialization)],
383 ) -> Result<Vec<KernelId>> {
384 self.usable()?;
385 let pending = specifications
386 .iter()
387 .map(|(source, specialization)| self.cache.request(source, specialization))
388 .collect::<Result<Vec<_>>>()?;
389 unsafe { self.cache.build(&self.stream)? };
391 let mut loaded = Vec::with_capacity(pending.len());
392 for request in pending {
393 loaded.push(unsafe { request.resolve(&self.stream)? }.clone());
395 }
396 self.synchronize()?;
397 let first = self.kernels.len();
398 self.kernels.extend(loaded);
399 Ok((first..self.kernels.len())
400 .map(|index| KernelId {
401 session: self.id,
402 index,
403 })
404 .collect())
405 }
406
407 #[must_use]
409 pub fn is_recorded(&self, key: usize) -> bool {
410 self.graphs.contains_key(&key)
411 }
412
413 pub unsafe fn record(&mut self, key: usize, commands: &[Command]) -> Result<()> {
424 self.usable()?;
425 if commands.is_empty() {
426 return Err(Error::Message("cannot record an empty model graph".into()));
427 }
428 if self.graphs.contains_key(&key) {
429 return Err(Error::Message(format!(
430 "model graph key {key} is already recorded"
431 )));
432 }
433 let prepared = self.prepare(commands)?;
434 let mut graph = self.stream.access_graph()?;
435 for command in &prepared {
436 match command {
437 PreparedCommand::Fill { region, value } => {
438 graph.fill(self.view(*region)?, *value)?;
439 }
440 PreparedCommand::Dispatch {
441 kernel,
442 constants,
443 grid,
444 bindings,
445 } => {
446 let kernel = self.kernel(*kernel)?;
447 let views = bindings
448 .iter()
449 .map(|binding| Ok(self.view(binding.region)?.access(binding.access)))
450 .collect::<Result<Vec<_>>>()?;
451 unsafe {
453 graph.dispatch(
454 kernel,
455 *grid,
456 kernel.info().workgroup_size,
457 constants,
458 &views,
459 )?
460 };
461 }
462 }
463 }
464 let executable = graph.finish()?;
465 self.graphs.insert(
466 key,
467 PreparedGraph {
468 executable,
469 commands: prepared,
470 },
471 );
472 Ok(())
473 }
474
475 pub fn upload(&mut self, region: Region, bytes: &[u8]) -> Result<()> {
477 self.usable()?;
478 if bytes.len() > region.bytes {
479 return Err(Error::Message("upload exceeds model region".into()));
480 }
481 self.validate_region(region)?;
482 let allocation = &self.allocations[region.allocation].buffer;
483 let view = allocation
484 .try_slice(region.offset, region.bytes)?
485 .slice(0, bytes.len())?;
486 let result = self.stream.upload(view, bytes);
487 if result.is_err() {
488 self.failed = true;
489 }
490 result
491 }
492
493 pub fn replay(&mut self, key: usize) -> Result<()> {
495 self.usable()?;
496 let graph = self
497 .graphs
498 .get_mut(&key)
499 .ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?;
500 let result = self.stream.launch(&mut graph.executable);
501 if result.is_err() {
502 self.failed = true;
503 }
504 result
505 }
506
507 pub fn synchronize(&mut self) -> Result<()> {
509 self.usable()?;
510 let result = self.stream.synchronize();
511 if result.is_err() {
512 self.failed = true;
513 }
514 result
515 }
516
517 pub fn reserve_readback(&mut self, bytes: usize) -> Result<()> {
522 self.usable()?;
523 if self
524 .readback
525 .as_ref()
526 .is_none_or(|buffer| buffer.bytes() < bytes)
527 {
528 self.readback = Some(self.stream.allocate_shared(bytes)?);
529 }
530 Ok(())
531 }
532
533 pub fn read(&mut self, region: Region, bytes: &mut [u8]) -> Result<()> {
535 self.read_many(&mut [(region, bytes)])
536 }
537
538 pub fn read_many(&mut self, outputs: &mut [(Region, &mut [u8])]) -> Result<()> {
540 self.usable()?;
541 let mut total = 0usize;
542 let mut all_shared = true;
543 for (region, output) in outputs.iter() {
544 self.validate_region(*region)?;
545 if output.len() > region.bytes {
546 return Err(Error::Message("readback exceeds model region".into()));
547 }
548 total = total
549 .checked_add(output.len())
550 .ok_or_else(|| Error::Message("readback size overflow".into()))?;
551 all_shared &= self.allocations[region.allocation].placement == Placement::Shared;
552 }
553 if all_shared {
554 self.synchronize()?;
555 for (region, output) in outputs.iter_mut() {
556 if output.is_empty() {
557 continue;
558 }
559 let pointer = self.allocations[region.allocation]
560 .buffer
561 .device_ptr()?
562 .cast::<u8>();
563 if pointer.is_null() {
564 self.failed = true;
565 return Err(Error::Message("null shared model pointer".into()));
566 }
567 unsafe {
570 std::ptr::copy_nonoverlapping(
571 pointer.add(region.offset),
572 output.as_mut_ptr(),
573 output.len(),
574 );
575 }
576 }
577 return Ok(());
578 }
579 self.reserve_readback(total)?;
580 {
581 let readback = self.readback.as_ref().expect("reserved above");
582 let mut offset = 0;
583 for (region, output) in outputs.iter() {
584 if !output.is_empty() {
585 let source = self.allocations[region.allocation]
586 .buffer
587 .try_slice(region.offset, output.len())?;
588 if let Err(error) = self
589 .stream
590 .copy(readback.try_slice(offset, output.len())?, source)
591 {
592 self.failed = true;
593 return Err(error);
594 }
595 }
596 offset += output.len();
597 }
598 }
599 self.synchronize()?;
600 if total == 0 {
601 return Ok(());
602 }
603 let readback = self.readback.as_ref().expect("reserved above");
604 let pointer = readback.device_ptr()?.cast::<u8>();
605 if pointer.is_null() {
606 self.failed = true;
607 return Err(Error::Message("null model readback pointer".into()));
608 }
609 let mut offset = 0;
610 for (_, output) in outputs.iter_mut() {
611 if !output.is_empty() {
612 unsafe {
615 std::ptr::copy_nonoverlapping(
616 pointer.add(offset),
617 output.as_mut_ptr(),
618 output.len(),
619 );
620 }
621 }
622 offset += output.len();
623 }
624 Ok(())
625 }
626
627 pub fn benchmark(&mut self, key: usize, samples: usize) -> Result<ForwardTimings> {
632 if samples < 10 {
633 return Err(Error::Message(
634 "use at least ten model timing samples".into(),
635 ));
636 }
637 self.usable()?;
638 let commands = self
639 .graphs
640 .get(&key)
641 .ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?
642 .commands
643 .clone();
644 let mut times = [Vec::with_capacity(samples), Vec::with_capacity(samples)];
645 for iteration in 0..samples + 10 {
646 for mode in [iteration % 2, 1 - iteration % 2] {
647 let start = Instant::now();
648 if mode == 0 {
649 self.replay(key)?;
650 } else {
651 let result = unsafe { self.execute_direct(&commands) };
653 if result.is_err() {
654 self.failed = true;
655 }
656 result?;
657 }
658 self.synchronize()?;
659 if iteration >= 10 {
660 times[mode].push(start.elapsed().as_secs_f64() * 1_000.0);
661 }
662 }
663 }
664 let [graph, direct] = times;
665 Ok(ForwardTimings {
666 samples,
667 graph: Distribution::from_samples(graph)?,
668 direct: Distribution::from_samples(direct)?,
669 })
670 }
671
672 pub fn profile_commands(&mut self, key: usize, samples: usize) -> Result<Vec<Distribution>> {
679 if samples == 0 {
680 return Err(Error::Message(
681 "use at least one command timing sample".into(),
682 ));
683 }
684 self.usable()?;
685 let commands = self
686 .graphs
687 .get(&key)
688 .ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?
689 .commands
690 .clone();
691 let mut distributions = Vec::with_capacity(commands.len());
692 for command in commands {
693 let warmup = unsafe { self.execute_direct(std::slice::from_ref(&command)) };
695 if warmup.is_err() {
696 self.failed = true;
697 }
698 warmup?;
699 self.synchronize()?;
700 let mut times = Vec::with_capacity(samples);
701 for _ in 0..samples {
702 let start = Instant::now();
703 let result = unsafe { self.execute_direct(std::slice::from_ref(&command)) };
705 if result.is_err() {
706 self.failed = true;
707 }
708 result?;
709 self.synchronize()?;
710 times.push(start.elapsed().as_secs_f64() * 1_000.0);
711 }
712 distributions.push(Distribution::from_samples(times)?);
713 }
714 Ok(distributions)
715 }
716
717 fn usable(&self) -> Result<()> {
718 if self.failed {
719 Err(Error::Message(
720 "model session is unusable after a GPU failure".into(),
721 ))
722 } else {
723 Ok(())
724 }
725 }
726
727 fn validate_region(&self, region: Region) -> Result<()> {
728 if region.session != self.id {
729 return Err(Error::Message(
730 "model region belongs to another session".into(),
731 ));
732 }
733 let allocation = self
734 .allocations
735 .get(region.allocation)
736 .ok_or_else(|| Error::Message("invalid model allocation".into()))?;
737 let end = region
738 .offset
739 .checked_add(region.bytes)
740 .ok_or_else(|| Error::Message("model region size overflow".into()))?;
741 if end > allocation.buffer.bytes() {
742 return Err(Error::Message("model region exceeds its allocation".into()));
743 }
744 Ok(())
745 }
746
747 fn view(&self, region: Region) -> Result<crate::View<'_>> {
748 self.validate_region(region)?;
749 self.allocations[region.allocation]
750 .buffer
751 .try_slice(region.offset, region.bytes)
752 }
753
754 fn views<'a>(&'a self, bindings: &[Binding]) -> Result<Vec<crate::View<'a>>> {
755 bindings
756 .iter()
757 .map(|binding| self.view(binding.region))
758 .collect()
759 }
760
761 pub fn launch_config(
765 &self,
766 id: KernelId,
767 workload_bits: &[u64],
768 ) -> Result<crate::loom::LaunchConfig> {
769 self.usable()?;
770 self.kernel(id)?.launch_config(workload_bits)
771 }
772
773 fn kernel(&self, id: KernelId) -> Result<&Kernel> {
774 if id.session != self.id {
775 return Err(Error::Message(
776 "model kernel belongs to another session".into(),
777 ));
778 }
779 self.kernels
780 .get(id.index)
781 .ok_or_else(|| Error::Message("invalid model kernel".into()))
782 }
783
784 fn prepare(&self, commands: &[Command]) -> Result<Vec<PreparedCommand>> {
785 commands
786 .iter()
787 .map(|command| match command {
788 Command::Fill { region, value } => {
789 self.validate_region(*region)?;
790 if region.is_empty() {
791 return Err(Error::Message("cannot record an empty model fill".into()));
792 }
793 Ok(PreparedCommand::Fill {
794 region: *region,
795 value: *value,
796 })
797 }
798 Command::Dispatch(dispatch) => {
799 let kernel = self.kernel(dispatch.kernel)?;
800 for binding in &dispatch.bindings {
801 self.validate_region(binding.region)?;
802 }
803 let constants = match &dispatch.arguments {
804 Arguments::Indices(values) => Box::new(Constants::indices(kernel, values)?),
805 Arguments::Packed(constants) => constants.clone(),
806 };
807 Ok(PreparedCommand::Dispatch {
808 kernel: dispatch.kernel,
809 constants,
810 grid: dispatch.grid,
811 bindings: dispatch.bindings.clone(),
812 })
813 }
814 })
815 .collect()
816 }
817
818 unsafe fn execute_direct(&self, commands: &[PreparedCommand]) -> Result<()> {
819 for command in commands {
820 match command {
821 PreparedCommand::Fill { region, value } => {
822 self.stream.fill(self.view(*region)?, *value)?;
823 }
824 PreparedCommand::Dispatch {
825 kernel,
826 constants,
827 grid,
828 bindings,
829 } => {
830 let kernel = self.kernel(*kernel)?;
831 let views = self.views(bindings)?;
832 unsafe {
834 self.stream.dispatch(
835 kernel,
836 *grid,
837 kernel.info().workgroup_size,
838 constants,
839 &views,
840 )?;
841 }
842 }
843 }
844 }
845 Ok(())
846 }
847}
848
849#[derive(Clone, Copy, Debug)]
850struct ScratchLifetime {
851 bytes: usize,
852 first: usize,
853 last: usize,
854}
855
856#[derive(Clone, Debug, Default)]
858pub struct ScratchPlanner<K> {
859 values: BTreeMap<K, ScratchLifetime>,
860}
861
862impl<K: Clone + Ord> ScratchPlanner<K> {
863 #[must_use]
865 pub fn new() -> Self {
866 Self {
867 values: BTreeMap::new(),
868 }
869 }
870
871 pub fn insert(&mut self, key: K, bytes: usize, first: usize, last: usize) -> Result<()> {
873 if bytes == 0 {
874 return Err(Error::Message("scratch values must be nonempty".into()));
875 }
876 if first > last {
877 return Err(Error::Message(
878 "scratch lifetime ends before it begins".into(),
879 ));
880 }
881 if self
882 .values
883 .insert(key, ScratchLifetime { bytes, first, last })
884 .is_some()
885 {
886 return Err(Error::Message("duplicate scratch value".into()));
887 }
888 Ok(())
889 }
890
891 pub fn finish(self) -> ScratchPlan<K> {
893 let mut values = self.values.into_iter().collect::<Vec<_>>();
894 values.sort_by(|(left_key, left), (right_key, right)| {
895 (left.first, std::cmp::Reverse(left.bytes), left_key).cmp(&(
896 right.first,
897 std::cmp::Reverse(right.bytes),
898 right_key,
899 ))
900 });
901 let mut active = Vec::<(usize, usize)>::new();
902 let mut free = BTreeSet::<usize>::new();
903 let mut slots = Vec::<usize>::new();
904 let mut assignments = BTreeMap::new();
905 for (key, lifetime) in values {
906 active.retain(|&(last, slot)| {
907 if last < lifetime.first {
908 free.insert(slot);
909 false
910 } else {
911 true
912 }
913 });
914 let slot = free
915 .iter()
916 .copied()
917 .filter(|&slot| slots[slot] >= lifetime.bytes)
918 .min_by_key(|&slot| slots[slot])
919 .map_or_else(
920 || {
921 slots.push(lifetime.bytes);
922 slots.len() - 1
923 },
924 |slot| {
925 free.remove(&slot);
926 slot
927 },
928 );
929 assignments.insert(key, slot);
930 active.push((lifetime.last, slot));
931 }
932 ScratchPlan { assignments, slots }
933 }
934}
935
936#[derive(Clone, Debug)]
938pub struct ScratchPlan<K> {
939 assignments: BTreeMap<K, usize>,
940 slots: Vec<usize>,
941}
942
943impl<K: Ord> ScratchPlan<K> {
944 #[must_use]
946 pub fn slots(&self) -> &[usize] {
947 &self.slots
948 }
949
950 pub fn slot(&self, key: &K) -> Result<usize> {
952 self.assignments
953 .get(key)
954 .copied()
955 .ok_or_else(|| Error::Message("scratch value was not planned".into()))
956 }
957
958 #[must_use]
960 pub fn assignments(&self) -> &BTreeMap<K, usize> {
961 &self.assignments
962 }
963}
964
965#[derive(Clone, Copy, Debug, serde::Serialize)]
967pub struct ForwardTimings {
968 pub samples: usize,
970 pub graph: Distribution,
972 pub direct: Distribution,
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use std::ops::Range;
980
981 fn region(allocation: usize, range: Range<usize>) -> Region {
982 Region {
983 session: 1,
984 allocation,
985 offset: range.start,
986 bytes: range.end - range.start,
987 }
988 }
989
990 #[test]
991 fn region_slices_are_relative_and_checked() {
992 let whole = region(2, 100..200);
993 let slice = whole.slice(25, 50).unwrap();
994 assert_eq!(slice.offset(), 125);
995 assert_eq!(slice.len(), 50);
996 assert!(whole.slice(101, 0).is_err());
997 assert!(whole.slice(usize::MAX, 2).is_err());
998 }
999
1000 #[test]
1001 fn distributions_use_nearest_rank_quantiles() {
1002 let distribution = Distribution::from_samples((1..=100).map(f64::from).collect()).unwrap();
1003 assert_eq!(distribution.median_ms, 50.0);
1004 assert_eq!(distribution.p95_ms, 95.0);
1005 }
1006
1007 #[test]
1008 fn scratch_slots_reuse_only_after_the_last_use() {
1009 let mut planner = ScratchPlanner::new();
1010 planner.insert("a", 8, 0, 1).unwrap();
1011 planner.insert("b", 16, 1, 2).unwrap();
1012 planner.insert("c", 4, 2, 3).unwrap();
1013 planner.insert("d", 7, 3, 3).unwrap();
1014 let plan = planner.finish();
1015 assert_ne!(plan.slot(&"a").unwrap(), plan.slot(&"b").unwrap());
1016 assert_eq!(plan.slot(&"a").unwrap(), plan.slot(&"c").unwrap());
1017 assert_eq!(plan.slot(&"b").unwrap(), plan.slot(&"d").unwrap());
1018 }
1019}