1use crate::cluster::NodeStatus;
9use crate::cluster::{ClusterState, NodeMetrics};
10use crate::protocol::NodeResources;
11
12pub const DELIVERY_RATIO_INT8_THRESHOLD: f32 = 0.95;
14pub const DELIVERY_RATIO_INT4_THRESHOLD: f32 = 0.80;
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct LayerSpec {
19 pub index: usize,
21 pub vram_gb: f32,
23 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#[derive(Clone, Debug, PartialEq)]
39pub struct LayerAssignment {
40 pub node_id: String,
42 pub start_layer: usize,
44 pub end_layer: usize,
46 pub used_vram_gb: f32,
48 pub num_layers: usize,
50}
51
52impl LayerAssignment {
53 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum QuantizationMode {
77 None,
79 Int8,
81 Int4,
83}
84
85#[derive(Clone, Debug)]
87pub struct PlacementPlan {
88 pub assignments: Vec<LayerAssignment>,
90 pub quantization_mode: QuantizationMode,
92 pub total_layers: usize,
94 pub participating_nodes: Vec<String>,
96}
97
98impl PlacementPlan {
99 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 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
138pub 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
149pub 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 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 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 if let Some(assignment) = current_assignment {
205 assignments.push(assignment);
206 }
207
208 Ok(assignments)
209}
210
211pub 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 let assignments = assign_layers_sequentially(&nodes, layers)?;
224
225 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 let quantization_mode = select_quantization_mode(total_delivery_ratio);
235
236 Ok(PlacementPlan::new(assignments, quantization_mode))
237}
238
239pub fn rebalance_assignments(cluster: &ClusterState, plan: &mut PlacementPlan) -> bool {
241 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 continue;
249 }
250
251 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 potential_layers > 2 {
260 needs_rebalance = true;
261 break;
262 }
263 }
264 }
265 }
266
267 needs_rebalance
268}
269
270pub 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 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 metrics.set_streaming_layers(start_layer, end_layer);
287
288 Some(metrics)
289}
290
291pub 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 let avg_delivery_ratio =
301 active_nodes.iter().map(|m| m.delivery_ratio).sum::<f32>() / active_nodes.len() as f32;
302
303 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 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 assert!(!rebalance_assignments(&ClusterState::default(), &mut plan));
416 }
417}