1use crate::expert_cache::{CopyPlan, ExpertId, GatherPlan};
57use crate::residency::{BankResidency, CopyRoute, ResidencyError};
58
59#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct SlotGeometry {
68 pub num_layers: usize,
69 pub slots: usize,
74 pub row_bytes: Vec<usize>,
77}
78
79impl SlotGeometry {
80 pub fn banks(&self) -> usize {
81 self.row_bytes.len()
82 }
83
84 pub fn bytes(&self) -> u64 {
87 self.row_bytes
88 .iter()
89 .map(|b| *b as u64 * self.slots as u64)
90 .sum()
91 }
92}
93
94pub trait SlotDevice {
106 fn begin_plan(&mut self, route: CopyRoute) -> Result<(), String> {
117 let _ = route;
118 Ok(())
119 }
120
121 fn write_slot(&mut self, bank: usize, dst_slot: u32, src: &[u8]) -> Result<(), String>;
125
126 fn copy_slot(&mut self, bank: usize, dst_slot: u32, src_slot: u32) -> Result<(), String>;
131
132 fn flush(&mut self) -> Result<(), String> {
135 Ok(())
136 }
137}
138
139pub trait ExpertRows {
148 fn row(&self, bank: usize, layer: u32, row: u32) -> Option<&[u8]>;
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
156pub struct Applied {
157 pub rows: u64,
159 pub bytes: u64,
160 pub warm: bool,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
169pub struct SlotStats {
170 pub plans: u64,
172 pub warm_plans: u64,
174 pub host_rows: u64,
176 pub host_bytes: u64,
177 pub device_rows: u64,
179 pub device_bytes: u64,
180}
181
182impl SlotStats {
183 pub fn warm_plan_rate(&self) -> f64 {
187 if self.plans == 0 {
188 return 0.0;
189 }
190 self.warm_plans as f64 / self.plans as f64
191 }
192
193 pub fn host_bytes_per_plan(&self) -> f64 {
196 if self.plans == 0 {
197 return 0.0;
198 }
199 self.host_bytes as f64 / self.plans as f64
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum SlotFault {
206 PlanHalvesDisagree { dst_slots: usize, src_rows: usize },
211 SlotOutOfRange { slot: u32, slots: usize },
214 SlotWrittenTwice { slot: u32 },
218 LayerOutOfRange { layer: u32, num_layers: usize },
220 RowMissing { bank: usize, layer: u32, row: u32 },
222 RowSizeMismatch {
225 bank: usize,
226 layer: u32,
227 row: u32,
228 expected: usize,
229 got: usize,
230 },
231 Residency(ResidencyError),
234 Device {
237 bank: usize,
238 slot: u32,
239 detail: String,
240 },
241 DeviceFlush { slots: Vec<u32>, detail: String },
250}
251
252impl std::fmt::Display for SlotFault {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 SlotFault::PlanHalvesDisagree {
256 dst_slots,
257 src_rows,
258 } => write!(
259 f,
260 "copy plan has {dst_slots} destination slots and {src_rows} source rows: the \
261 pairs do not line up, so applying it would load experts into each other's slots"
262 ),
263 SlotFault::SlotOutOfRange { slot, slots } => write!(
264 f,
265 "plan names slot {slot} but the pool has {slots}: the planner and the pool were \
266 built with different cache sizes"
267 ),
268 SlotFault::SlotWrittenTwice { slot } => write!(
269 f,
270 "plan writes slot {slot} twice: one of the two experts would be absent from the \
271 slot the plan promised it in"
272 ),
273 SlotFault::LayerOutOfRange { layer, num_layers } => write!(
274 f,
275 "plan names layer {layer} but the pool was built for {num_layers}"
276 ),
277 SlotFault::RowMissing { bank, layer, row } => {
278 write!(f, "bank {bank} has no row {row} for layer {layer}")
279 }
280 SlotFault::RowSizeMismatch {
281 bank,
282 layer,
283 row,
284 expected,
285 got,
286 } => write!(
287 f,
288 "bank {bank} row {row} of layer {layer} is {got} bytes, not {expected}: a \
289 short row would leave the slot's tail holding the previous occupant"
290 ),
291 SlotFault::Residency(e) => write!(f, "{e}"),
292 SlotFault::Device { bank, slot, detail } => write!(
293 f,
294 "device refused the copy into bank {bank} slot {slot}: {detail}"
295 ),
296 SlotFault::DeviceFlush { slots, detail } => write!(
297 f,
298 "device failed to complete {} deferred cop{}: {detail}; slots {slots:?} are all \
299 suspect, since the backend cannot say which landed",
300 slots.len(),
301 if slots.len() == 1 { "y" } else { "ies" },
302 ),
303 }
304 }
305}
306
307impl std::error::Error for SlotFault {}
308
309impl From<ResidencyError> for SlotFault {
310 fn from(e: ResidencyError) -> Self {
311 SlotFault::Residency(e)
312 }
313}
314
315pub struct ExpertSlots {
323 geometry: SlotGeometry,
324 residency: BankResidency,
325 occupant: Vec<Option<ExpertId>>,
326 stats: SlotStats,
327}
328
329impl ExpertSlots {
330 pub fn new(geometry: SlotGeometry) -> Result<Self, SlotFault> {
338 if geometry.slots == 0 {
339 return Err(SlotFault::SlotOutOfRange { slot: 0, slots: 0 });
340 }
341 if geometry.num_layers == 0 {
342 return Err(SlotFault::LayerOutOfRange {
343 layer: 0,
344 num_layers: 0,
345 });
346 }
347 for (bank, bytes) in geometry.row_bytes.iter().enumerate() {
348 if *bytes == 0 {
349 return Err(SlotFault::RowSizeMismatch {
350 bank,
351 layer: 0,
352 row: 0,
353 expected: 0,
354 got: 0,
355 });
356 }
357 }
358 let residency = BankResidency::all_pinned(geometry.num_layers);
359 Ok(ExpertSlots {
360 occupant: vec![None; geometry.slots],
361 geometry,
362 residency,
363 stats: SlotStats::default(),
364 })
365 }
366
367 pub fn with_residency(mut self, residency: BankResidency) -> Self {
374 self.residency = residency;
375 self
376 }
377
378 pub fn geometry(&self) -> &SlotGeometry {
379 &self.geometry
380 }
381
382 pub fn stats(&self) -> SlotStats {
383 self.stats
384 }
385
386 pub fn reset_stats(&mut self) {
387 self.stats = SlotStats::default();
388 }
389
390 pub fn occupant(&self, slot: u32) -> Option<ExpertId> {
394 self.occupant.get(slot as usize).copied().flatten()
395 }
396
397 pub fn occupied(&self) -> usize {
399 self.occupant.iter().filter(|o| o.is_some()).count()
400 }
401
402 pub fn invalidate_slot(&mut self, slot: u32) {
404 if let Some(o) = self.occupant.get_mut(slot as usize) {
405 *o = None;
406 }
407 }
408
409 pub fn invalidate_all(&mut self) {
412 self.occupant.iter_mut().for_each(|o| *o = None);
413 }
414
415 pub fn resize(&mut self, slots: usize) -> Result<(), SlotFault> {
424 if slots == 0 {
425 return Err(SlotFault::SlotOutOfRange { slot: 0, slots: 0 });
426 }
427 self.geometry.slots = slots;
428 self.occupant = vec![None; slots];
429 Ok(())
430 }
431
432 pub fn apply_copy_plan(
436 &mut self,
437 layer: u32,
438 plan: &CopyPlan,
439 rows: &dyn ExpertRows,
440 device: &mut dyn SlotDevice,
441 ) -> Result<Applied, SlotFault> {
442 self.apply_plan(layer, plan, false, rows, device)
443 }
444
445 pub fn apply_materialize(
451 &mut self,
452 layer: u32,
453 plan: &CopyPlan,
454 rows: &dyn ExpertRows,
455 device: &mut dyn SlotDevice,
456 ) -> Result<Applied, SlotFault> {
457 self.apply_plan(layer, plan, true, rows, device)
458 }
459
460 fn apply_plan(
461 &mut self,
462 layer: u32,
463 plan: &CopyPlan,
464 whole_layer: bool,
465 rows: &dyn ExpertRows,
466 device: &mut dyn SlotDevice,
467 ) -> Result<Applied, SlotFault> {
468 if plan.dst_slots.len() != plan.src_rows.len() {
469 return Err(SlotFault::PlanHalvesDisagree {
470 dst_slots: plan.dst_slots.len(),
471 src_rows: plan.src_rows.len(),
472 });
473 }
474 if layer as usize >= self.geometry.num_layers {
475 return Err(SlotFault::LayerOutOfRange {
476 layer,
477 num_layers: self.geometry.num_layers,
478 });
479 }
480 let route = self.residency.copy_route(layer, whole_layer)?;
484
485 self.validate_slots(&plan.dst_slots)?;
486 self.validate_rows(layer, &plan.src_rows, rows)?;
487
488 self.stats.plans += 1;
489 if plan.is_empty() {
490 self.stats.warm_plans += 1;
491 return Ok(Applied {
492 rows: 0,
493 bytes: 0,
494 warm: true,
495 });
496 }
497
498 device
499 .begin_plan(route)
500 .map_err(|detail| SlotFault::DeviceFlush {
501 slots: plan.dst_slots.clone(),
502 detail,
503 })?;
504
505 let mut applied = Applied::default();
506 for (&dst, &src) in plan.dst_slots.iter().zip(plan.src_rows.iter()) {
507 self.occupant[dst as usize] = None;
511 for bank in 0..self.geometry.banks() {
512 let bytes = rows
513 .row(bank, layer, src)
514 .expect("validated by validate_rows");
515 device
516 .write_slot(bank, dst, bytes)
517 .map_err(|detail| SlotFault::Device {
518 bank,
519 slot: dst,
520 detail,
521 })?;
522 applied.rows += 1;
523 applied.bytes += bytes.len() as u64;
524 }
525 self.occupant[dst as usize] = Some(ExpertId { layer, expert: src });
526 }
527 self.flush(device, &plan.dst_slots)?;
528
529 self.stats.host_rows += applied.rows;
530 self.stats.host_bytes += applied.bytes;
531 Ok(applied)
532 }
533
534 fn flush(&mut self, device: &mut dyn SlotDevice, written: &[u32]) -> Result<(), SlotFault> {
537 let Err(detail) = device.flush() else {
538 return Ok(());
539 };
540 for &slot in written {
541 self.invalidate_slot(slot);
542 }
543 Err(SlotFault::DeviceFlush {
544 slots: written.to_vec(),
545 detail,
546 })
547 }
548
549 pub fn apply_gather_plan(
557 &mut self,
558 plan: &GatherPlan,
559 device: &mut dyn SlotDevice,
560 ) -> Result<Applied, SlotFault> {
561 if plan.dst_slots.len() != plan.src_slots.len() {
562 return Err(SlotFault::PlanHalvesDisagree {
563 dst_slots: plan.dst_slots.len(),
564 src_rows: plan.src_slots.len(),
565 });
566 }
567 self.validate_slots(&plan.dst_slots)?;
568 for &src in &plan.src_slots {
569 if src as usize >= self.geometry.slots {
570 return Err(SlotFault::SlotOutOfRange {
571 slot: src,
572 slots: self.geometry.slots,
573 });
574 }
575 if self.occupant(src).is_none() {
576 return Err(SlotFault::Device {
577 bank: 0,
578 slot: src,
579 detail: "gather source holds no known expert; a failed copy would be \
580 propagated into a second slot"
581 .to_string(),
582 });
583 }
584 }
585
586 self.stats.plans += 1;
587 if plan.is_empty() {
588 self.stats.warm_plans += 1;
589 return Ok(Applied {
590 rows: 0,
591 bytes: 0,
592 warm: true,
593 });
594 }
595
596 let mut applied = Applied::default();
597 for (&dst, &src) in plan.dst_slots.iter().zip(plan.src_slots.iter()) {
598 let carried = self.occupant(src);
599 self.occupant[dst as usize] = None;
600 for bank in 0..self.geometry.banks() {
601 device
602 .copy_slot(bank, dst, src)
603 .map_err(|detail| SlotFault::Device {
604 bank,
605 slot: dst,
606 detail,
607 })?;
608 applied.rows += 1;
609 applied.bytes += self.geometry.row_bytes[bank] as u64;
610 }
611 self.occupant[dst as usize] = carried;
612 }
613 self.flush(device, &plan.dst_slots)?;
614
615 self.stats.device_rows += applied.rows;
616 self.stats.device_bytes += applied.bytes;
617 Ok(applied)
618 }
619
620 fn validate_slots(&self, slots: &[u32]) -> Result<(), SlotFault> {
621 for (i, &slot) in slots.iter().enumerate() {
622 if slot as usize >= self.geometry.slots {
623 return Err(SlotFault::SlotOutOfRange {
624 slot,
625 slots: self.geometry.slots,
626 });
627 }
628 if slots[..i].contains(&slot) {
632 return Err(SlotFault::SlotWrittenTwice { slot });
633 }
634 }
635 Ok(())
636 }
637
638 fn validate_rows(
639 &self,
640 layer: u32,
641 src_rows: &[u32],
642 rows: &dyn ExpertRows,
643 ) -> Result<(), SlotFault> {
644 for &row in src_rows {
645 for (bank, &expected) in self.geometry.row_bytes.iter().enumerate() {
646 let Some(bytes) = rows.row(bank, layer, row) else {
647 return Err(SlotFault::RowMissing { bank, layer, row });
648 };
649 if bytes.len() != expected {
650 return Err(SlotFault::RowSizeMismatch {
651 bank,
652 layer,
653 row,
654 expected,
655 got: bytes.len(),
656 });
657 }
658 }
659 }
660 Ok(())
661 }
662}
663
664pub struct HostSlotMemory {
672 banks: Vec<Vec<u8>>,
673 row_bytes: Vec<usize>,
674}
675
676impl HostSlotMemory {
677 pub fn new(geometry: &SlotGeometry) -> Self {
678 HostSlotMemory {
679 banks: geometry
680 .row_bytes
681 .iter()
682 .map(|b| vec![0u8; b * geometry.slots])
683 .collect(),
684 row_bytes: geometry.row_bytes.clone(),
685 }
686 }
687
688 pub fn slot(&self, bank: usize, slot: u32) -> &[u8] {
692 let w = self.row_bytes[bank];
693 let at = w * slot as usize;
694 &self.banks[bank][at..at + w]
695 }
696}
697
698impl SlotDevice for HostSlotMemory {
699 fn write_slot(&mut self, bank: usize, dst_slot: u32, src: &[u8]) -> Result<(), String> {
700 let w = self.row_bytes[bank];
701 let at = w * dst_slot as usize;
702 self.banks[bank][at..at + w].copy_from_slice(src);
703 Ok(())
704 }
705
706 fn copy_slot(&mut self, bank: usize, dst_slot: u32, src_slot: u32) -> Result<(), String> {
707 let w = self.row_bytes[bank];
708 let (dst, src) = (w * dst_slot as usize, w * src_slot as usize);
709 self.banks[bank].copy_within(src..src + w, dst);
710 Ok(())
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use crate::expert_cache::ExpertCache;
718 use crate::residency::HostResidency;
719
720 const LAYERS: usize = 3;
721 const EXPERTS: usize = 8;
722 const GATE: usize = 6;
723 const UP: usize = 6;
724 const DOWN: usize = 4;
725
726 fn geometry(slots: usize) -> SlotGeometry {
727 SlotGeometry {
728 num_layers: LAYERS,
729 slots,
730 row_bytes: vec![GATE, UP, DOWN],
731 }
732 }
733
734 struct NamedRows {
738 banks: Vec<Vec<Vec<u8>>>,
741 }
742
743 impl NamedRows {
744 fn new() -> Self {
745 let banks = [GATE, UP, DOWN]
746 .iter()
747 .enumerate()
748 .map(|(bank, &width)| {
749 (0..LAYERS as u32)
750 .flat_map(|layer| {
751 (0..EXPERTS as u32).map(move |row| named_row(bank, width, layer, row))
752 })
753 .collect()
754 })
755 .collect();
756 NamedRows { banks }
757 }
758
759 fn expected(&self, bank: usize, layer: u32, row: u32) -> &[u8] {
760 &self.banks[bank][layer as usize * EXPERTS + row as usize]
761 }
762 }
763
764 fn named_row(bank: usize, width: usize, layer: u32, row: u32) -> Vec<u8> {
768 (0..width)
769 .map(|i| {
770 (bank as u8 + 1)
771 .wrapping_mul(37)
772 .wrapping_add(layer as u8)
773 .wrapping_add((row as u8) << 3)
774 ^ i as u8
775 })
776 .collect()
777 }
778
779 impl ExpertRows for NamedRows {
780 fn row(&self, bank: usize, layer: u32, row: u32) -> Option<&[u8]> {
781 if bank >= self.banks.len() || layer as usize >= LAYERS || row as usize >= EXPERTS {
782 return None;
783 }
784 Some(self.expected(bank, layer, row))
785 }
786 }
787
788 #[test]
793 fn a_step_that_hits_the_cache_copies_nothing() {
794 let mut cache = ExpertCache::new(LAYERS, EXPERTS, 32);
795 let mut slots = ExpertSlots::new(geometry(32)).unwrap();
796 let rows = NamedRows::new();
797 let mut device = HostSlotMemory::new(slots.geometry());
798
799 let routed = [1u32, 4, 6];
800 let cold = cache.ensure(0, &routed);
801 let first = slots
802 .apply_copy_plan(0, &cold.copy, &rows, &mut device)
803 .unwrap();
804 assert!(!first.warm);
805 assert_eq!(first.rows, 3 * 3, "three experts across three banks");
806 assert_eq!(first.bytes as usize, 3 * (GATE + UP + DOWN));
807
808 let before = slots.stats();
809 for _ in 0..10 {
810 let warm = cache.ensure(0, &routed);
811 assert!(warm.copy.is_empty(), "the cache should report every hit");
812 let applied = slots
813 .apply_copy_plan(0, &warm.copy, &rows, &mut device)
814 .unwrap();
815 assert!(applied.warm);
816 assert_eq!(applied.bytes, 0);
817 }
818 let after = slots.stats();
819 assert_eq!(
820 after.host_bytes, before.host_bytes,
821 "ten warm steps moved bytes"
822 );
823 assert_eq!(after.warm_plans, before.warm_plans + 10);
824 assert_eq!(after.warm_plan_rate(), 10.0 / 11.0);
825 }
826
827 #[test]
831 fn every_slot_holds_the_expert_the_plan_promised() {
832 let mut cache = ExpertCache::new(LAYERS, EXPERTS, 16);
833 let mut slots = ExpertSlots::new(geometry(16)).unwrap();
834 let rows = NamedRows::new();
835 let mut device = HostSlotMemory::new(slots.geometry());
836
837 for layer in 0..LAYERS as u32 {
838 let routed: Vec<u32> = (0..4).map(|e| (e + layer) % EXPERTS as u32).collect();
839 let plan = cache.ensure(layer, &routed);
840 slots
841 .apply_copy_plan(layer, &plan.copy, &rows, &mut device)
842 .unwrap();
843
844 for (&expert, slot) in routed.iter().zip(plan.slots.iter()) {
845 let slot = slot.expect("pure offload places every route");
846 assert_eq!(
847 slots.occupant(slot),
848 Some(ExpertId { layer, expert }),
849 "layer {layer} expert {expert}"
850 );
851 for bank in 0..3 {
852 assert_eq!(
853 device.slot(bank, slot),
854 rows.expected(bank, layer, expert),
855 "layer {layer} expert {expert} bank {bank}"
856 );
857 }
858 }
859 }
860 assert_eq!(slots.occupied(), 12, "three layers of four experts");
861 }
862
863 #[test]
867 fn an_evicted_slot_is_overwritten_and_not_merely_relabelled() {
868 let mut cache = ExpertCache::new(2, 2, 2);
873 let mut slots = ExpertSlots::new(SlotGeometry {
874 num_layers: 2,
875 slots: 2,
876 row_bytes: vec![GATE, UP, DOWN],
877 })
878 .unwrap();
879 let rows = NamedRows::new();
880 let mut device = HostSlotMemory::new(slots.geometry());
881
882 let first = cache.ensure(0, &[0, 1]);
883 slots
884 .apply_copy_plan(0, &first.copy, &rows, &mut device)
885 .unwrap();
886 let second = cache.ensure(1, &[0, 1]);
887 assert_eq!(second.missing, 2, "layer 1 must evict layer 0");
888 slots
889 .apply_copy_plan(1, &second.copy, &rows, &mut device)
890 .unwrap();
891
892 for (&expert, slot) in [0u32, 1].iter().zip(second.slots.iter()) {
893 let slot = slot.unwrap();
894 assert_eq!(slots.occupant(slot), Some(ExpertId { layer: 1, expert }));
895 assert_eq!(
896 device.slot(0, slot),
897 rows.expected(0, 1, expert),
898 "the slot must hold layer 1's bytes, not layer 0's"
899 );
900 }
901 assert_eq!(cache.slot_of(0, 0), None, "layer 0's expert 0 was evicted");
902 }
903
904 #[test]
909 fn a_plan_whose_halves_disagree_is_refused_untouched() {
910 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
911 let rows = NamedRows::new();
912 let mut device = HostSlotMemory::new(slots.geometry());
913 let plan = CopyPlan {
914 dst_slots: vec![0, 1, 2],
915 src_rows: vec![0, 1],
916 };
917 assert_eq!(
918 slots.apply_copy_plan(0, &plan, &rows, &mut device),
919 Err(SlotFault::PlanHalvesDisagree {
920 dst_slots: 3,
921 src_rows: 2,
922 })
923 );
924 assert_eq!(slots.occupied(), 0);
925 assert_eq!(
926 slots.stats().plans,
927 0,
928 "a refused plan is not an applied one"
929 );
930 }
931
932 #[test]
936 fn a_plan_that_writes_one_slot_twice_is_refused() {
937 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
938 let rows = NamedRows::new();
939 let mut device = HostSlotMemory::new(slots.geometry());
940 let plan = CopyPlan {
941 dst_slots: vec![3, 1, 3],
942 src_rows: vec![0, 1, 2],
943 };
944 assert_eq!(
945 slots.apply_copy_plan(0, &plan, &rows, &mut device),
946 Err(SlotFault::SlotWrittenTwice { slot: 3 })
947 );
948 assert_eq!(slots.occupied(), 0);
949 }
950
951 #[test]
955 fn a_slot_the_pool_does_not_have_is_refused() {
956 let mut slots = ExpertSlots::new(geometry(4)).unwrap();
957 let rows = NamedRows::new();
958 let mut device = HostSlotMemory::new(slots.geometry());
959 let plan = CopyPlan {
960 dst_slots: vec![0, 9],
961 src_rows: vec![0, 1],
962 };
963 assert_eq!(
964 slots.apply_copy_plan(0, &plan, &rows, &mut device),
965 Err(SlotFault::SlotOutOfRange { slot: 9, slots: 4 })
966 );
967 assert_eq!(slots.occupied(), 0);
968 }
969
970 #[test]
974 fn a_row_that_is_not_exactly_one_slot_wide_is_refused() {
975 struct ShortDownBank;
976 impl ExpertRows for ShortDownBank {
977 fn row(&self, bank: usize, _layer: u32, _row: u32) -> Option<&[u8]> {
978 match bank {
979 0 => Some(&[0u8; GATE]),
980 1 => Some(&[0u8; UP]),
981 _ => Some(&[0u8; DOWN - 1]),
982 }
983 }
984 }
985 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
986 let mut device = HostSlotMemory::new(slots.geometry());
987 let plan = CopyPlan {
988 dst_slots: vec![0],
989 src_rows: vec![5],
990 };
991 assert_eq!(
992 slots.apply_copy_plan(0, &plan, &ShortDownBank, &mut device),
993 Err(SlotFault::RowSizeMismatch {
994 bank: 2,
995 layer: 0,
996 row: 5,
997 expected: DOWN,
998 got: DOWN - 1,
999 })
1000 );
1001 assert_eq!(slots.occupied(), 0, "nothing was written");
1002 }
1003
1004 #[test]
1008 fn a_missing_host_row_names_its_bank() {
1009 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1010 let rows = NamedRows::new();
1011 let mut device = HostSlotMemory::new(slots.geometry());
1012 let plan = CopyPlan {
1013 dst_slots: vec![0],
1014 src_rows: vec![EXPERTS as u32],
1015 };
1016 assert_eq!(
1017 slots.apply_copy_plan(0, &plan, &rows, &mut device),
1018 Err(SlotFault::RowMissing {
1019 bank: 0,
1020 layer: 0,
1021 row: EXPERTS as u32,
1022 })
1023 );
1024 }
1025
1026 #[test]
1032 fn an_unpinned_layer_refuses_an_lru_remap_but_takes_a_materialize() {
1033 let residency = BankResidency::new(
1037 &[
1038 HostResidency::Pinned,
1039 HostResidency::Pageable,
1040 HostResidency::Pinned,
1041 ],
1042 LAYERS,
1043 &[1u32].into_iter().collect(),
1044 false,
1045 )
1046 .unwrap();
1047 let mut slots = ExpertSlots::new(geometry(EXPERTS * LAYERS))
1048 .unwrap()
1049 .with_residency(residency);
1050 let rows = NamedRows::new();
1051 let mut device = HostSlotMemory::new(slots.geometry());
1052
1053 let remap = CopyPlan {
1054 dst_slots: vec![0],
1055 src_rows: vec![3],
1056 };
1057 assert!(matches!(
1058 slots.apply_copy_plan(1, &remap, &rows, &mut device),
1059 Err(SlotFault::Residency(
1060 ResidencyError::SlotRemapOnUnpinnedLayer { layer: 1 }
1061 ))
1062 ));
1063
1064 let whole = CopyPlan {
1065 dst_slots: (0..EXPERTS as u32).collect(),
1066 src_rows: (0..EXPERTS as u32).collect(),
1067 };
1068 let applied = slots
1069 .apply_materialize(1, &whole, &rows, &mut device)
1070 .unwrap();
1071 assert_eq!(applied.rows, EXPERTS as u64 * 3);
1072
1073 assert!(slots.apply_copy_plan(0, &remap, &rows, &mut device).is_ok());
1075 }
1076
1077 #[test]
1082 fn a_device_fault_leaves_its_slot_unknown_and_names_it() {
1083 struct FailsOnDownBank;
1084 impl SlotDevice for FailsOnDownBank {
1085 fn write_slot(&mut self, bank: usize, _d: u32, _s: &[u8]) -> Result<(), String> {
1086 if bank == 2 {
1087 return Err("out of device memory".to_string());
1088 }
1089 Ok(())
1090 }
1091 fn copy_slot(&mut self, _b: usize, _d: u32, _s: u32) -> Result<(), String> {
1092 Ok(())
1093 }
1094 }
1095 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1096 let rows = NamedRows::new();
1097 let plan = CopyPlan {
1098 dst_slots: vec![5],
1099 src_rows: vec![2],
1100 };
1101 let err = slots
1102 .apply_copy_plan(0, &plan, &rows, &mut FailsOnDownBank)
1103 .unwrap_err();
1104 assert_eq!(
1105 err,
1106 SlotFault::Device {
1107 bank: 2,
1108 slot: 5,
1109 detail: "out of device memory".to_string(),
1110 }
1111 );
1112 assert_eq!(
1113 slots.occupant(5),
1114 None,
1115 "a slot whose copy failed must not read back as resident"
1116 );
1117 }
1118
1119 #[test]
1127 fn a_failing_flush_forgets_every_slot_the_plan_wrote() {
1128 struct FlushFails;
1129 impl SlotDevice for FlushFails {
1130 fn write_slot(&mut self, _b: usize, _d: u32, _s: &[u8]) -> Result<(), String> {
1131 Ok(())
1132 }
1133 fn copy_slot(&mut self, _b: usize, _d: u32, _s: u32) -> Result<(), String> {
1134 Ok(())
1135 }
1136 fn flush(&mut self) -> Result<(), String> {
1137 Err("copy engine reported an error".to_string())
1138 }
1139 }
1140 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1141 let rows = NamedRows::new();
1142 let plan = CopyPlan {
1143 dst_slots: vec![1, 4, 6],
1144 src_rows: vec![0, 2, 3],
1145 };
1146 assert_eq!(
1147 slots.apply_copy_plan(0, &plan, &rows, &mut FlushFails),
1148 Err(SlotFault::DeviceFlush {
1149 slots: vec![1, 4, 6],
1150 detail: "copy engine reported an error".to_string(),
1151 })
1152 );
1153 assert_eq!(
1154 slots.occupied(),
1155 0,
1156 "no slot may read back as resident after an unconfirmed flush"
1157 );
1158 }
1159
1160 #[test]
1165 fn a_gather_is_counted_separately_from_host_traffic() {
1166 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1167 let rows = NamedRows::new();
1168 let mut device = HostSlotMemory::new(slots.geometry());
1169 slots
1170 .apply_copy_plan(
1171 0,
1172 &CopyPlan {
1173 dst_slots: vec![4],
1174 src_rows: vec![6],
1175 },
1176 &rows,
1177 &mut device,
1178 )
1179 .unwrap();
1180
1181 let gather = GatherPlan {
1182 dst_slots: vec![1],
1183 src_slots: vec![4],
1184 };
1185 let applied = slots.apply_gather_plan(&gather, &mut device).unwrap();
1186 assert_eq!(applied.rows, 3);
1187 assert_eq!(applied.bytes as usize, GATE + UP + DOWN);
1188
1189 let stats = slots.stats();
1190 assert_eq!(stats.device_bytes as usize, GATE + UP + DOWN);
1191 assert_eq!(
1192 stats.host_bytes as usize,
1193 GATE + UP + DOWN,
1194 "the gather must not be billed to the link"
1195 );
1196 assert_eq!(
1197 slots.occupant(1),
1198 Some(ExpertId {
1199 layer: 0,
1200 expert: 6
1201 }),
1202 "the gathered slot carries the source's identity"
1203 );
1204 assert_eq!(device.slot(1, 1), rows.expected(1, 0, 6));
1205 }
1206
1207 #[test]
1212 fn a_gather_from_an_unknown_slot_is_refused() {
1213 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1214 let mut device = HostSlotMemory::new(slots.geometry());
1215 let gather = GatherPlan {
1216 dst_slots: vec![0],
1217 src_slots: vec![7],
1218 };
1219 assert!(matches!(
1220 slots.apply_gather_plan(&gather, &mut device),
1221 Err(SlotFault::Device { slot: 7, .. })
1222 ));
1223 }
1224
1225 #[test]
1229 fn a_resize_drops_residency_but_keeps_the_counters() {
1230 let mut slots = ExpertSlots::new(geometry(8)).unwrap();
1231 let rows = NamedRows::new();
1232 let mut device = HostSlotMemory::new(slots.geometry());
1233 slots
1234 .apply_copy_plan(
1235 0,
1236 &CopyPlan {
1237 dst_slots: vec![0, 1],
1238 src_rows: vec![0, 1],
1239 },
1240 &rows,
1241 &mut device,
1242 )
1243 .unwrap();
1244 let before = slots.stats();
1245 assert_eq!(slots.occupied(), 2);
1246
1247 slots.resize(64).unwrap();
1248 assert_eq!(slots.occupied(), 0);
1249 assert_eq!(slots.geometry().slots, 64);
1250 assert_eq!(slots.stats(), before);
1251 assert_eq!(
1252 slots.resize(0),
1253 Err(SlotFault::SlotOutOfRange { slot: 0, slots: 0 })
1254 );
1255 }
1256
1257 #[test]
1261 fn a_degenerate_geometry_is_refused_at_construction() {
1262 assert!(ExpertSlots::new(geometry(0)).is_err());
1263 assert!(ExpertSlots::new(SlotGeometry {
1264 num_layers: 0,
1265 slots: 4,
1266 row_bytes: vec![GATE],
1267 })
1268 .is_err());
1269 assert!(ExpertSlots::new(SlotGeometry {
1270 num_layers: 1,
1271 slots: 4,
1272 row_bytes: vec![GATE, 0],
1273 })
1274 .is_err());
1275 }
1276
1277 #[test]
1280 fn the_geometry_reports_the_device_bytes_it_needs() {
1281 let g = geometry(1024);
1282 assert_eq!(g.bytes(), 1024 * (GATE + UP + DOWN) as u64);
1283 assert_eq!(g.banks(), 3);
1284 }
1285
1286 #[test]
1290 fn a_forgotten_slot_is_refetched_by_the_next_step() {
1291 let mut cache = ExpertCache::new(1, EXPERTS, 8);
1292 let mut slots = ExpertSlots::new(SlotGeometry {
1293 num_layers: 1,
1294 slots: 8,
1295 row_bytes: vec![GATE, UP, DOWN],
1296 })
1297 .unwrap();
1298 let rows = NamedRows::new();
1299 let mut device = HostSlotMemory::new(slots.geometry());
1300
1301 let plan = cache.ensure(0, &[2]);
1302 slots
1303 .apply_copy_plan(0, &plan.copy, &rows, &mut device)
1304 .unwrap();
1305 let slot = plan.slots[0].unwrap();
1306
1307 assert_eq!(
1308 cache.forget_slot(slot),
1309 Some(ExpertId {
1310 layer: 0,
1311 expert: 2
1312 })
1313 );
1314 slots.invalidate_slot(slot);
1315
1316 let again = cache.ensure(0, &[2]);
1317 assert_eq!(again.missing, 1, "a forgotten expert must miss");
1318 assert!(!again.copy.is_empty(), "and must be re-fetched");
1319 slots
1320 .apply_copy_plan(0, &again.copy, &rows, &mut device)
1321 .unwrap();
1322 assert_eq!(
1323 slots.occupant(again.slots[0].unwrap()),
1324 Some(ExpertId {
1325 layer: 0,
1326 expert: 2
1327 })
1328 );
1329 assert_eq!(
1330 again.slots[0],
1331 Some(slot),
1332 "a forgotten slot is the first candidate, so the re-fetch reclaims \
1333 it rather than spending a slot that really holds an expert"
1334 );
1335
1336 let empty = (0..8).find(|&s| cache.resident_in(s).is_none()).unwrap();
1340 assert_eq!(cache.forget_slot(empty), None);
1341 assert_eq!(cache.forget_slot(9_999), None, "and an unknown slot too");
1342 }
1343}