1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct NodeWeight {
15 pub node_id: u64,
17 pub compute_cost: u64,
19 pub memory_bytes: u64,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct GraphEdge {
26 pub from: u64,
28 pub to: u64,
30 pub data_bytes: u64,
32}
33
34#[derive(Debug, Clone)]
36pub struct Partition {
37 pub partition_id: usize,
39 pub node_ids: Vec<u64>,
41 pub total_compute: u64,
43 pub total_memory: u64,
45}
46
47impl Partition {
48 pub fn size(&self) -> usize {
50 self.node_ids.len()
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct PartitionStats {
57 pub num_partitions: usize,
59 pub cut_edges: usize,
61 pub cut_bytes: u64,
63 pub compute_imbalance: f64,
66}
67
68impl PartitionStats {
69 pub fn is_balanced(&self, threshold: f64) -> bool {
71 self.compute_imbalance < threshold
72 }
73}
74
75#[derive(Debug, Default)]
82pub struct TensorGraphPartitioner {
83 pub nodes: HashMap<u64, NodeWeight>,
85 pub edges: Vec<GraphEdge>,
87}
88
89impl TensorGraphPartitioner {
90 pub fn new() -> Self {
92 Self {
93 nodes: HashMap::new(),
94 edges: Vec::new(),
95 }
96 }
97
98 pub fn add_node(&mut self, node: NodeWeight) {
100 self.nodes.insert(node.node_id, node);
101 }
102
103 pub fn add_edge(&mut self, edge: GraphEdge) {
105 self.edges.push(edge);
106 }
107
108 pub fn remove_node(&mut self, node_id: u64) -> bool {
113 self.nodes.remove(&node_id).is_some()
114 }
115
116 pub fn partition(&self, k: usize) -> Vec<Partition> {
127 if k == 0 || self.nodes.is_empty() {
128 return Vec::new();
129 }
130
131 let mut sorted_nodes: Vec<&NodeWeight> = self.nodes.values().collect();
134 sorted_nodes.sort_unstable_by(|a, b| {
135 b.compute_cost
136 .cmp(&a.compute_cost)
137 .then_with(|| a.node_id.cmp(&b.node_id))
138 });
139
140 let num_partitions = k.min(sorted_nodes.len());
141
142 let mut partitions: Vec<Partition> = (0..num_partitions)
144 .map(|pid| Partition {
145 partition_id: pid,
146 node_ids: Vec::new(),
147 total_compute: 0,
148 total_memory: 0,
149 })
150 .collect();
151
152 if num_partitions == sorted_nodes.len() {
153 for (node, partition) in sorted_nodes.iter().zip(partitions.iter_mut()) {
155 partition.node_ids.push(node.node_id);
156 partition.total_compute = node.compute_cost;
157 partition.total_memory = node.memory_bytes;
158 }
159 } else {
160 for node in &sorted_nodes {
163 let target = partitions
164 .iter()
165 .enumerate()
166 .min_by_key(|(_, p)| p.total_compute)
167 .map(|(i, _)| i)
168 .unwrap_or(0); partitions[target].node_ids.push(node.node_id);
171 partitions[target].total_compute = partitions[target]
172 .total_compute
173 .saturating_add(node.compute_cost);
174 partitions[target].total_memory = partitions[target]
175 .total_memory
176 .saturating_add(node.memory_bytes);
177 }
178 }
179
180 partitions.retain(|p| !p.node_ids.is_empty());
182 partitions
183 }
184
185 pub fn stats(&self, partitions: &[Partition]) -> PartitionStats {
187 let mut node_to_partition: HashMap<u64, usize> = HashMap::new();
189 for partition in partitions {
190 for &nid in &partition.node_ids {
191 node_to_partition.insert(nid, partition.partition_id);
192 }
193 }
194
195 let mut cut_edges: usize = 0;
197 let mut cut_bytes: u64 = 0;
198 for edge in &self.edges {
199 let from_part = node_to_partition.get(&edge.from);
200 let to_part = node_to_partition.get(&edge.to);
201 match (from_part, to_part) {
202 (Some(fp), Some(tp)) if fp != tp => {
203 cut_edges += 1;
204 cut_bytes = cut_bytes.saturating_add(edge.data_bytes);
205 }
206 _ => {}
207 }
208 }
209
210 let num_partitions = partitions.len();
212 let compute_imbalance = if num_partitions <= 1 {
213 0.0_f64
214 } else {
215 let computes: Vec<u64> = partitions.iter().map(|p| p.total_compute).collect();
216 let max_c = computes.iter().copied().max().unwrap_or(0);
217 let min_c = computes.iter().copied().min().unwrap_or(0);
218 let avg_c: f64 = computes.iter().copied().sum::<u64>() as f64 / num_partitions as f64;
219 if avg_c == 0.0 {
220 0.0
221 } else {
222 (max_c - min_c) as f64 / avg_c
223 }
224 };
225
226 PartitionStats {
227 num_partitions,
228 cut_edges,
229 cut_bytes,
230 compute_imbalance,
231 }
232 }
233}
234
235#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn node(id: u64, compute: u64, mem: u64) -> NodeWeight {
245 NodeWeight {
246 node_id: id,
247 compute_cost: compute,
248 memory_bytes: mem,
249 }
250 }
251
252 fn edge(from: u64, to: u64, bytes: u64) -> GraphEdge {
254 GraphEdge {
255 from,
256 to,
257 data_bytes: bytes,
258 }
259 }
260
261 #[test]
263 fn test_empty_graph_returns_empty() {
264 let p = TensorGraphPartitioner::new();
265 assert!(p.partition(4).is_empty());
266 }
267
268 #[test]
270 fn test_k_zero_returns_empty() {
271 let mut p = TensorGraphPartitioner::new();
272 p.add_node(node(1, 100, 64));
273 assert!(p.partition(0).is_empty());
274 }
275
276 #[test]
278 fn test_single_partition_contains_all_nodes() {
279 let mut p = TensorGraphPartitioner::new();
280 for i in 1..=5_u64 {
281 p.add_node(node(i, i * 10, i * 8));
282 }
283 let parts = p.partition(1);
284 assert_eq!(parts.len(), 1);
285 assert_eq!(parts[0].size(), 5);
286 assert_eq!(parts[0].total_compute, 150);
288 assert_eq!(parts[0].total_memory, 120);
290 }
291
292 #[test]
294 fn test_k_greater_than_nodes_one_per_partition() {
295 let mut p = TensorGraphPartitioner::new();
296 p.add_node(node(10, 50, 8));
297 p.add_node(node(20, 50, 8));
298 p.add_node(node(30, 50, 8));
299 let parts = p.partition(10);
300 assert_eq!(parts.len(), 3);
301 for part in &parts {
302 assert_eq!(part.size(), 1);
303 }
304 }
305
306 #[test]
308 fn test_k_equals_node_count() {
309 let mut p = TensorGraphPartitioner::new();
310 p.add_node(node(1, 100, 10));
311 p.add_node(node(2, 200, 20));
312 let parts = p.partition(2);
313 assert_eq!(parts.len(), 2);
314 assert!(parts.iter().all(|pt| pt.size() == 1));
315 }
316
317 #[test]
320 fn test_greedy_balancing_reduces_imbalance() {
321 let mut p = TensorGraphPartitioner::new();
322 p.add_node(node(1, 100, 0));
324 p.add_node(node(2, 100, 0));
325 p.add_node(node(3, 50, 0));
326 p.add_node(node(4, 50, 0));
327 let parts = p.partition(2);
328 assert_eq!(parts.len(), 2);
329 assert_eq!(parts[0].total_compute, 150);
330 assert_eq!(parts[1].total_compute, 150);
331 }
332
333 #[test]
335 fn test_stats_single_partition_zero_imbalance() {
336 let mut p = TensorGraphPartitioner::new();
337 p.add_node(node(1, 500, 64));
338 let parts = p.partition(1);
339 let s = p.stats(&parts);
340 assert_eq!(s.num_partitions, 1);
341 assert!((s.compute_imbalance - 0.0).abs() < f64::EPSILON);
342 }
343
344 #[test]
347 fn test_compute_imbalance_formula() {
348 let mut p = TensorGraphPartitioner::new();
349 p.add_node(node(1, 300, 0));
351 p.add_node(node(2, 100, 0));
352 let parts = p.partition(2);
353 let s = p.stats(&parts);
354 assert!(
356 (s.compute_imbalance - 1.0).abs() < 1e-9,
357 "imbalance={}",
358 s.compute_imbalance
359 );
360 }
361
362 #[test]
364 fn test_is_balanced_threshold() {
365 let mut p = TensorGraphPartitioner::new();
366 p.add_node(node(1, 300, 0));
367 p.add_node(node(2, 100, 0));
368 let parts = p.partition(2);
369 let s = p.stats(&parts);
370 assert!(s.is_balanced(1.1));
372 assert!(!s.is_balanced(0.9));
373 }
374
375 #[test]
377 fn test_cut_edges_same_partition_not_cut() {
378 let mut p = TensorGraphPartitioner::new();
379 p.add_node(node(1, 100, 0));
380 p.add_node(node(2, 50, 0));
381 p.add_edge(edge(1, 2, 1024));
383 let parts = p.partition(1);
384 let s = p.stats(&parts);
385 assert_eq!(s.cut_edges, 0);
386 assert_eq!(s.cut_bytes, 0);
387 }
388
389 #[test]
391 fn test_cut_edges_cross_partition() {
392 let mut p = TensorGraphPartitioner::new();
393 p.add_node(node(1, 100, 0));
394 p.add_node(node(2, 100, 0));
395 p.add_edge(edge(1, 2, 512));
396 let parts = p.partition(2);
398 let s = p.stats(&parts);
399 assert_eq!(s.cut_edges, 1);
400 assert_eq!(s.cut_bytes, 512);
401 }
402
403 #[test]
405 fn test_cut_bytes_sum() {
406 let mut p = TensorGraphPartitioner::new();
407 p.add_node(node(1, 100, 0));
408 p.add_node(node(2, 100, 0));
409 p.add_edge(edge(1, 2, 200));
410 p.add_edge(edge(1, 2, 300));
411 let parts = p.partition(2);
412 let s = p.stats(&parts);
413 assert_eq!(s.cut_bytes, 500);
415 }
416
417 #[test]
419 fn test_edges_with_unknown_nodes_ignored() {
420 let mut p = TensorGraphPartitioner::new();
421 p.add_node(node(1, 100, 0));
422 p.add_edge(edge(1, 99, 999));
424 let parts = p.partition(1);
425 let s = p.stats(&parts);
426 assert_eq!(s.cut_edges, 0);
427 assert_eq!(s.cut_bytes, 0);
428 }
429
430 #[test]
432 fn test_remove_node_present() {
433 let mut p = TensorGraphPartitioner::new();
434 p.add_node(node(42, 10, 8));
435 assert!(p.remove_node(42));
436 assert!(!p.nodes.contains_key(&42));
437 }
438
439 #[test]
441 fn test_remove_node_absent() {
442 let mut p = TensorGraphPartitioner::new();
443 assert!(!p.remove_node(999));
444 }
445
446 #[test]
448 fn test_add_edge_appended() {
449 let mut p = TensorGraphPartitioner::new();
450 p.add_edge(edge(1, 2, 64));
451 p.add_edge(edge(2, 3, 128));
452 assert_eq!(p.edges.len(), 2);
453 assert_eq!(p.edges[0].data_bytes, 64);
454 assert_eq!(p.edges[1].data_bytes, 128);
455 }
456
457 #[test]
459 fn test_partition_size_method() {
460 let part = Partition {
461 partition_id: 0,
462 node_ids: vec![1, 2, 3],
463 total_compute: 300,
464 total_memory: 48,
465 };
466 assert_eq!(part.size(), 3);
467 }
468
469 #[test]
471 fn test_stats_no_edges_zero_cuts() {
472 let mut p = TensorGraphPartitioner::new();
473 for i in 1..=4_u64 {
474 p.add_node(node(i, i * 100, i * 8));
475 }
476 let parts = p.partition(2);
477 let s = p.stats(&parts);
478 assert_eq!(s.cut_edges, 0);
479 assert_eq!(s.cut_bytes, 0);
480 }
481
482 #[test]
486 fn test_greedy_largest_first_assignment() {
487 let mut p = TensorGraphPartitioner::new();
488 p.add_node(node(1, 1000, 0));
489 p.add_node(node(2, 1, 0));
490 p.add_node(node(3, 1, 0));
491 p.add_node(node(4, 1, 0));
492 let parts = p.partition(2);
493 assert_eq!(parts.len(), 2);
494 let computes: Vec<u64> = parts.iter().map(|p| p.total_compute).collect();
495 assert!(computes.contains(&1000));
497 assert!(computes.contains(&3));
498 }
499
500 #[test]
502 fn test_stats_num_partitions_matches_slice() {
503 let mut p = TensorGraphPartitioner::new();
504 for i in 1..=6_u64 {
505 p.add_node(node(i, 10, 8));
506 }
507 let parts = p.partition(3);
508 let s = p.stats(&parts);
509 assert_eq!(s.num_partitions, parts.len());
510 }
511}