Skip to main content

ghostlink_core/
planning.rs

1//! Greedy Layer Assignment with Fault Tolerance and Adaptive Quantization
2//!
3//! This module provides:
4//! - Sequential greedy layer splitting across nodes based on VRAM capacity
5//! - Adaptive quantization trigger (select_quantization_mode)
6//! - Load balancing and fault detection integration
7
8use crate::cluster::NodeStatus;
9use crate::cluster::{ClusterState, NodeMetrics};
10use crate::protocol::NodeResources;
11
12/// Delivery ratio thresholds for adaptive quantization
13pub const DELIVERY_RATIO_INT8_THRESHOLD: f32 = 0.95;
14pub const DELIVERY_RATIO_INT4_THRESHOLD: f32 = 0.80;
15
16/// Layer specification with VRAM requirements
17#[derive(Clone, Debug, PartialEq)]
18pub struct LayerSpec {
19    /// Layer index (0-based)
20    pub index: usize,
21    /// VRAM required in GB
22    pub vram_gb: f32,
23    /// Number of weights in the layer
24    pub num_weights: u32,
25}
26
27impl Default for LayerSpec {
28    fn default() -> Self {
29        Self {
30            index: 0,
31            vram_gb: 1.0,
32            num_weights: 0,
33        }
34    }
35}
36
37/// Layer assignment to a specific node
38#[derive(Clone, Debug, PartialEq)]
39pub struct LayerAssignment {
40    /// Node ID
41    pub node_id: String,
42    /// Start layer index (inclusive)
43    pub start_layer: usize,
44    /// End layer index (exclusive)
45    pub end_layer: usize,
46    /// VRAM used on this node
47    pub used_vram_gb: f32,
48    /// Number of layers assigned
49    pub num_layers: usize,
50}
51
52impl LayerAssignment {
53    /// Create new layer assignment
54    pub fn new(node_id: String, start_layer: usize, end_layer: usize, vram_gb: f32) -> Self {
55        Self {
56            node_id,
57            start_layer,
58            end_layer,
59            used_vram_gb: vram_gb,
60            num_layers: end_layer - start_layer,
61        }
62    }
63
64    /// Get average VRAM per layer
65    pub fn avg_vram_per_layer(&self) -> f32 {
66        if self.num_layers == 0 {
67            0.0
68        } else {
69            self.used_vram_gb / self.num_layers as f32
70        }
71    }
72}
73
74/// Quantization mode enumeration
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum QuantizationMode {
77    /// No quantization (full precision)
78    None,
79    /// 8-bit quantization
80    Int8,
81    /// 4-bit quantization
82    Int4,
83}
84
85/// Layer placement plan across nodes
86#[derive(Clone, Debug)]
87pub struct PlacementPlan {
88    /// Assignments per node
89    pub assignments: Vec<LayerAssignment>,
90    /// Selected quantization mode
91    pub quantization_mode: QuantizationMode,
92    /// Total layers assigned
93    pub total_layers: usize,
94    /// Nodes participating in plan
95    pub participating_nodes: Vec<String>,
96}
97
98impl PlacementPlan {
99    /// Create new placement plan
100    pub fn new(assignments: Vec<LayerAssignment>, quantization_mode: QuantizationMode) -> Self {
101        let participating_nodes = assignments.iter().map(|a| a.node_id.clone()).collect();
102        let total_layers = assignments.iter().map(|a| a.num_layers).sum();
103
104        Self {
105            assignments,
106            quantization_mode,
107            total_layers,
108            participating_nodes,
109        }
110    }
111
112    /// Get human-readable plan summary
113    pub fn summary(&self) -> String {
114        let mode_str = match self.quantization_mode {
115            QuantizationMode::None => "Full Precision",
116            QuantizationMode::Int8 => "8-bit Quantized",
117            QuantizationMode::Int4 => "4-bit Quantized",
118        };
119
120        format!(
121            "Placement Plan ({})\n\
122             =================\n\
123             Total layers: {}\n\
124             Quantization: {}\n\
125             Nodes: {}\n",
126            mode_str,
127            self.total_layers,
128            match self.quantization_mode {
129                QuantizationMode::None => "Full Precision".to_string(),
130                QuantizationMode::Int8 => "8-bit Quantized".to_string(),
131                QuantizationMode::Int4 => "4-bit Quantized".to_string(),
132            },
133            self.participating_nodes.join(", ")
134        )
135    }
136}
137
138/// Select quantization mode based on cluster health metrics
139pub fn select_quantization_mode(delivery_ratio: f32) -> QuantizationMode {
140    if delivery_ratio >= DELIVERY_RATIO_INT8_THRESHOLD {
141        QuantizationMode::None
142    } else if delivery_ratio >= DELIVERY_RATIO_INT4_THRESHOLD {
143        QuantizationMode::Int8
144    } else {
145        QuantizationMode::Int4
146    }
147}
148
149/// Assign layers sequentially across nodes based on VRAM capacity
150pub fn assign_layers_sequentially(
151    nodes: &[NodeResources],
152    layers: &[LayerSpec],
153) -> Result<Vec<LayerAssignment>, String> {
154    if nodes.is_empty() {
155        return Err("at least one node is required".into());
156    }
157    if layers.is_empty() {
158        return Ok(Vec::new());
159    }
160
161    let mut assignments = Vec::new();
162    let mut current_node_index = 0usize;
163    let mut remaining_capacity = nodes[0].vram_gb;
164    let mut current_assignment: Option<LayerAssignment> = None;
165
166    for layer in layers {
167        while layer.vram_gb > remaining_capacity {
168            // Need to move to next node
169            if let Some(assignment) = current_assignment.take() {
170                assignments.push(assignment);
171            }
172
173            current_node_index += 1;
174            if current_node_index >= nodes.len() {
175                return Err(format!(
176                    "insufficient cluster VRAM for layer {} (needs {:.2} GB)",
177                    layer.index, layer.vram_gb
178                ));
179            }
180            remaining_capacity = nodes[current_node_index].vram_gb;
181        }
182
183        // Assign layer to current node
184        remaining_capacity -= layer.vram_gb;
185
186        match current_assignment.as_mut() {
187            Some(assignment) => {
188                assignment.end_layer = layer.index + 1;
189                assignment.used_vram_gb += layer.vram_gb;
190                assignment.num_layers += 1;
191            }
192            None => {
193                current_assignment = Some(LayerAssignment::new(
194                    nodes[current_node_index].id.clone(),
195                    layer.index,
196                    layer.index + 1,
197                    layer.vram_gb,
198                ));
199            }
200        }
201    }
202
203    // Finalize last assignment
204    if let Some(assignment) = current_assignment {
205        assignments.push(assignment);
206    }
207
208    Ok(assignments)
209}
210
211/// Assign layers with fault tolerance and load balancing
212pub fn assign_layers_with_fault_tolerance(
213    cluster: &ClusterState,
214    layers: &[LayerSpec],
215) -> Result<PlacementPlan, String> {
216    let nodes = cluster.nodes_snapshot();
217
218    if nodes.is_empty() {
219        return Err("no nodes available".into());
220    }
221
222    // First pass: greedy assignment
223    let assignments = assign_layers_sequentially(&nodes, layers)?;
224
225    // Calculate average delivery ratio across all nodes
226    let total_delivery_ratio = cluster
227        .active_nodes()
228        .iter()
229        .map(|m| m.delivery_ratio)
230        .sum::<f32>()
231        / cluster.active_nodes().len() as f32;
232
233    // Select quantization mode based on health
234    let quantization_mode = select_quantization_mode(total_delivery_ratio);
235
236    Ok(PlacementPlan::new(assignments, quantization_mode))
237}
238
239/// Update layer assignments based on node health metrics
240pub fn rebalance_assignments(cluster: &ClusterState, plan: &mut PlacementPlan) -> bool {
241    // Check if any active node has high available VRAM
242    let mut needs_rebalance = false;
243
244    for assignment in &mut plan.assignments {
245        if let Some(metrics) = cluster.get_metrics(&assignment.node_id) {
246            if metrics.status != NodeStatus::Active {
247                // Skip failed nodes
248                continue;
249            }
250
251            // Check if this node can take more layers
252            let available = metrics.available_vram_gb;
253            let avg_layer_size = assignment.avg_vram_per_layer();
254
255            if available > 0.0 && avg_layer_size > 0.0 {
256                let potential_layers = (available / avg_layer_size) as usize;
257
258                // If node has significant capacity, mark for rebalancing
259                if potential_layers > 2 {
260                    needs_rebalance = true;
261                    break;
262                }
263            }
264        }
265    }
266
267    needs_rebalance
268}
269
270/// Simulate layer streaming on a node with metrics updates
271pub fn simulate_layer_streaming(
272    node_id: &str,
273    cluster: &ClusterState,
274    start_layer: usize,
275    end_layer: usize,
276) -> Option<NodeMetrics> {
277    let mut metrics = cluster.get_metrics(node_id)?;
278
279    // Record VRAM usage
280    let num_layers = end_layer - start_layer;
281    let avg_vram = (metrics.available_vram_gb + metrics.vram_gb) / 2.0;
282    let vram_per_layer = avg_vram / num_layers as f32;
283    metrics.record_vram_usage(vram_per_layer * num_layers as f32);
284
285    // Set streaming layers
286    metrics.set_streaming_layers(start_layer, end_layer);
287
288    Some(metrics)
289}
290
291/// Calculate network health across the cluster
292pub fn calculate_cluster_health(cluster: &ClusterState) -> (f32, usize, Vec<String>) {
293    let active_nodes = cluster.active_nodes();
294
295    if active_nodes.is_empty() {
296        return (0.0, 0, vec![]);
297    }
298
299    // Calculate average delivery ratio
300    let avg_delivery_ratio =
301        active_nodes.iter().map(|m| m.delivery_ratio).sum::<f32>() / active_nodes.len() as f32;
302
303    // Count failed nodes
304    let failed_count = cluster
305        .nodes_snapshot()
306        .iter()
307        .filter(|n| {
308            if let Some(metrics) = cluster.get_metrics(&n.id) {
309                metrics.status == NodeStatus::Failed
310            } else {
311                false
312            }
313        })
314        .count();
315
316    // Get failed node IDs
317    let failed_nodes: Vec<String> = cluster
318        .nodes_snapshot()
319        .iter()
320        .filter(|n| {
321            if let Some(metrics) = cluster.get_metrics(&n.id) {
322                metrics.status == NodeStatus::Failed
323            } else {
324                false
325            }
326        })
327        .map(|n| n.id.clone())
328        .collect();
329
330    (avg_delivery_ratio, failed_count, failed_nodes)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn sample_layers(count: usize, vram_gb: f32) -> Vec<LayerSpec> {
338        (0..count)
339            .map(|index| LayerSpec {
340                index,
341                vram_gb,
342                num_weights: 0,
343            })
344            .collect()
345    }
346
347    #[test]
348    fn greedily_places_layers_across_nodes() {
349        let nodes = vec![
350            NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
351            NodeResources::new("node-b", 12.0, 32.0, "8.6", None),
352        ];
353
354        let assignments = assign_layers_sequentially(&nodes, &sample_layers(33, 1.0)).unwrap();
355
356        assert_eq!(
357            assignments,
358            vec![
359                LayerAssignment {
360                    node_id: "node-a".into(),
361                    start_layer: 0,
362                    end_layer: 24,
363                    used_vram_gb: 24.0,
364                    num_layers: 24,
365                },
366                LayerAssignment {
367                    node_id: "node-b".into(),
368                    start_layer: 24,
369                    end_layer: 33,
370                    used_vram_gb: 9.0,
371                    num_layers: 9,
372                }
373            ]
374        );
375    }
376
377    #[test]
378    fn reports_insufficient_capacity() {
379        let nodes = vec![NodeResources::new("node-a", 2.0, 64.0, "8.9", None)];
380        let error = assign_layers_sequentially(&nodes, &sample_layers(3, 1.0)).unwrap_err();
381
382        assert!(error.contains("insufficient cluster VRAM"));
383    }
384
385    #[test]
386    fn selects_quantization_mode_from_delivery_ratio() {
387        assert_eq!(select_quantization_mode(0.98), QuantizationMode::None);
388        assert_eq!(select_quantization_mode(0.90), QuantizationMode::Int8);
389        assert_eq!(select_quantization_mode(0.75), QuantizationMode::Int4);
390    }
391
392    #[test]
393    fn placement_plan_summary() {
394        let plan = PlacementPlan::new(
395            vec![
396                LayerAssignment::new("node-a".into(), 0, 24, 24.0),
397                LayerAssignment::new("node-b".into(), 24, 33, 9.0),
398            ],
399            QuantizationMode::None,
400        );
401
402        let summary = plan.summary();
403        assert!(summary.contains("Total layers: 33"));
404        assert!(summary.contains("Full Precision"));
405    }
406
407    #[test]
408    fn rebalance_assignments_detects_capacity() {
409        let mut plan = PlacementPlan::new(
410            vec![LayerAssignment::new("node-a".into(), 0, 24, 24.0)],
411            QuantizationMode::None,
412        );
413
414        // Would need actual cluster state to test rebalancing
415        assert!(!rebalance_assignments(&ClusterState::default(), &mut plan));
416    }
417}