use crate::cluster::ClusterState;
use crate::cluster::NodeStatus;
use crate::host::{AccelerationMode, RuntimeProfile};
use crate::protocol::NodeResources;
pub const DELIVERY_RATIO_INT8_THRESHOLD: f32 = 0.95;
pub const DELIVERY_RATIO_INT4_THRESHOLD: f32 = 0.80;
#[derive(Clone, Debug, PartialEq)]
pub struct LayerSpec {
pub index: usize,
pub vram_gb: f32,
pub num_weights: u32,
}
impl Default for LayerSpec {
fn default() -> Self {
Self {
index: 0,
vram_gb: 1.0,
num_weights: 0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayerAssignment {
pub node_id: String,
pub start_layer: usize,
pub end_layer: usize,
pub used_vram_gb: f32,
pub num_layers: usize,
}
impl LayerAssignment {
pub fn new(node_id: String, start_layer: usize, end_layer: usize, vram_gb: f32) -> Self {
Self {
node_id,
start_layer,
end_layer,
used_vram_gb: vram_gb,
num_layers: end_layer - start_layer,
}
}
pub fn avg_vram_per_layer(&self) -> f32 {
if self.num_layers == 0 {
0.0
} else {
self.used_vram_gb / self.num_layers as f32
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum QuantizationMode {
#[default]
None,
Int8,
Int4,
}
#[derive(Clone, Debug, Default)]
pub struct PlacementPlan {
pub assignments: Vec<LayerAssignment>,
pub quantization_mode: QuantizationMode,
pub total_layers: usize,
pub participating_nodes: Vec<String>,
}
impl PlacementPlan {
pub fn new(assignments: Vec<LayerAssignment>, quantization_mode: QuantizationMode) -> Self {
let mut participating_nodes = Vec::with_capacity(assignments.len());
let mut total_layers = 0usize;
for a in &assignments {
total_layers += a.num_layers;
if participating_nodes.last() != Some(&a.node_id)
&& !participating_nodes.contains(&a.node_id)
{
participating_nodes.push(a.node_id.clone());
}
}
Self {
assignments,
quantization_mode,
total_layers,
participating_nodes,
}
}
pub fn summary(&self) -> String {
let mode_str = match self.quantization_mode {
QuantizationMode::None => "Full Precision",
QuantizationMode::Int8 => "8-bit Quantized",
QuantizationMode::Int4 => "4-bit Quantized",
};
format!(
"Placement Plan ({})\n\
=================\n\
Total layers: {}\n\
Quantization: {}\n\
Nodes: {}\n",
mode_str,
self.total_layers,
mode_str,
self.participating_nodes.join(", ")
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlanningTuning {
pub max_layers_per_assignment: usize,
}
impl PlanningTuning {
pub fn from_runtime_profile(profile: &RuntimeProfile, total_layers: usize) -> Self {
let worker_count = profile.recommended_workers.max(1);
let (accelerator_bias, vector_bias) = match profile.acceleration_mode {
AccelerationMode::Gpu => (2, 2),
AccelerationMode::Avx512 => (1, 2),
_ => (0, 1),
};
let target_chunks = (worker_count + accelerator_bias + vector_bias - 1).max(1);
let chunk_size = if total_layers == 0 {
1
} else {
total_layers.div_ceil(target_chunks).max(1)
};
Self {
max_layers_per_assignment: chunk_size,
}
}
}
pub fn select_quantization_mode(delivery_ratio: f32) -> QuantizationMode {
if delivery_ratio >= DELIVERY_RATIO_INT8_THRESHOLD {
QuantizationMode::None
} else if delivery_ratio >= DELIVERY_RATIO_INT4_THRESHOLD {
QuantizationMode::Int8
} else {
QuantizationMode::Int4
}
}
pub fn assign_layers_sequentially(
nodes: &[NodeResources],
layers: &[LayerSpec],
) -> Result<Vec<LayerAssignment>, String> {
if nodes.is_empty() {
return Err("at least one node is required".into());
}
if layers.is_empty() {
return Ok(Vec::new());
}
let mut assignments = Vec::with_capacity(nodes.len());
let mut node_idx = 0usize;
let mut current_node = &nodes[0];
let mut remaining_capacity = current_node.vram_gb;
let mut current_start = 0usize;
let mut current_start_layer = layers[0].index;
let mut last_layer_index = layers[0].index;
let mut current_vram = 0.0f32;
for (i, layer) in layers.iter().enumerate() {
while layer.vram_gb > remaining_capacity {
if i > current_start {
let start_layer = current_start_layer;
let end_layer = last_layer_index + 1;
assignments.push(LayerAssignment::new(
current_node.id.clone(),
start_layer,
end_layer,
current_vram,
));
}
node_idx += 1;
if node_idx >= nodes.len() {
return Err(format!(
"insufficient cluster VRAM for layer {} (needs {:.2} GB)",
layer.index, layer.vram_gb
));
}
current_node = &nodes[node_idx];
remaining_capacity = current_node.vram_gb;
current_start = i;
current_start_layer = layer.index;
current_vram = 0.0;
}
remaining_capacity -= layer.vram_gb;
current_vram += layer.vram_gb;
last_layer_index = layer.index;
}
if current_start < layers.len() {
let start_layer = current_start_layer;
let end_layer = last_layer_index + 1;
assignments.push(LayerAssignment::new(
current_node.id.clone(),
start_layer,
end_layer,
current_vram,
));
}
Ok(assignments)
}
pub fn chunk_assignments_for_workers(
assignments: Vec<LayerAssignment>,
max_layers_per_assignment: usize,
) -> Vec<LayerAssignment> {
let chunk_size = max_layers_per_assignment.max(1);
let needs_chunking = assignments.iter().any(|a| a.num_layers > chunk_size);
if !needs_chunking {
return assignments;
}
let mut chunked = Vec::with_capacity(assignments.len() * 2);
for mut assignment in assignments {
if assignment.num_layers <= chunk_size {
chunked.push(assignment);
continue;
}
let avg_vram = assignment.avg_vram_per_layer();
let mut start_layer = assignment.start_layer;
while start_layer < assignment.end_layer {
let end_layer = (start_layer + chunk_size).min(assignment.end_layer);
let num_layers = end_layer - start_layer;
let node_id = if end_layer == assignment.end_layer {
std::mem::take(&mut assignment.node_id)
} else {
assignment.node_id.clone()
};
chunked.push(LayerAssignment::new(
node_id,
start_layer,
end_layer,
avg_vram * num_layers as f32,
));
start_layer = end_layer;
}
}
chunked
}
fn assign_layers_chunked(
nodes: &[NodeResources],
layers: &[LayerSpec],
max_layers_per_assignment: usize,
) -> Result<Vec<LayerAssignment>, String> {
if nodes.is_empty() {
return Err("at least one node is required".into());
}
if layers.is_empty() {
return Ok(Vec::new());
}
let chunk_size = max_layers_per_assignment.max(1);
let est_assignments = (layers.len().div_ceil(chunk_size)).max(nodes.len());
let mut assignments = Vec::with_capacity(est_assignments);
let mut node_idx = 0usize;
let mut current_node = &nodes[0];
let mut remaining_vram = current_node.vram_gb;
let mut chunk_start = 0usize;
let mut chunk_start_layer = layers[0].index;
let mut last_layer_index = layers[0].index;
let mut chunk_vram = 0.0f32;
for (i, layer) in layers.iter().enumerate() {
while layer.vram_gb > remaining_vram {
if i > chunk_start {
let start_layer = chunk_start_layer;
let end_layer = last_layer_index + 1;
assignments.push(LayerAssignment::new(
current_node.id.clone(),
start_layer,
end_layer,
chunk_vram,
));
}
node_idx += 1;
if node_idx >= nodes.len() {
return Err(format!(
"insufficient cluster VRAM for layer {} (needs {:.2} GB)",
layer.index, layer.vram_gb
));
}
current_node = &nodes[node_idx];
remaining_vram = current_node.vram_gb;
chunk_start = i;
chunk_vram = 0.0;
}
if i == chunk_start {
chunk_start_layer = layer.index;
}
remaining_vram -= layer.vram_gb;
chunk_vram += layer.vram_gb;
last_layer_index = layer.index;
let chunk_len = i + 1 - chunk_start;
if chunk_len >= chunk_size {
let start_layer = chunk_start_layer;
let end_layer = layer.index + 1;
assignments.push(LayerAssignment::new(
current_node.id.clone(),
start_layer,
end_layer,
chunk_vram,
));
chunk_start = i + 1;
chunk_vram = 0.0;
}
}
if chunk_start < layers.len() {
let start_layer = chunk_start_layer;
let end_layer = last_layer_index + 1;
assignments.push(LayerAssignment::new(
current_node.id.clone(),
start_layer,
end_layer,
chunk_vram,
));
}
Ok(assignments)
}
pub fn assign_layers_with_runtime_profile(
nodes: &[NodeResources],
layers: &[LayerSpec],
profile: &RuntimeProfile,
) -> Result<Vec<LayerAssignment>, String> {
let tuning = PlanningTuning::from_runtime_profile(profile, layers.len());
assign_layers_chunked(nodes, layers, tuning.max_layers_per_assignment)
}
fn calculate_average_delivery_ratio(cluster: &ClusterState) -> f32 {
let (sum_delivery, active_count) = {
let metrics = cluster
.metrics
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let mut sum = 0.0f32;
let mut count = 0usize;
for metric in metrics.values() {
if metric.status == NodeStatus::Active {
sum += metric.delivery_ratio;
count += 1;
}
}
(sum, count)
};
if active_count > 0 {
sum_delivery / active_count as f32
} else {
0.0
}
}
pub fn assign_layers_with_fault_tolerance(
cluster: &ClusterState,
layers: &[LayerSpec],
) -> Result<PlacementPlan, String> {
let nodes = cluster.nodes_snapshot();
if nodes.is_empty() {
return Err("no nodes available".into());
}
let assignments = assign_layers_sequentially(&nodes, layers)?;
let total_delivery_ratio = calculate_average_delivery_ratio(cluster);
let quantization_mode = select_quantization_mode(total_delivery_ratio);
Ok(PlacementPlan::new(assignments, quantization_mode))
}
pub fn assign_layers_with_fault_tolerance_and_runtime(
cluster: &ClusterState,
layers: &[LayerSpec],
profile: &RuntimeProfile,
) -> Result<PlacementPlan, String> {
let nodes = cluster.nodes_snapshot();
if nodes.is_empty() {
return Err("no nodes available".into());
}
let tuning = PlanningTuning::from_runtime_profile(profile, layers.len());
let assignments = assign_layers_chunked(&nodes, layers, tuning.max_layers_per_assignment)?;
let total_delivery_ratio = calculate_average_delivery_ratio(cluster);
let quantization_mode = select_quantization_mode(total_delivery_ratio);
Ok(PlacementPlan::new(assignments, quantization_mode))
}
pub fn rebalance_assignments(cluster: &ClusterState, plan: &mut PlacementPlan) -> bool {
let mut needs_rebalance = false;
let metrics = cluster
.metrics
.lock()
.unwrap_or_else(|poison| poison.into_inner());
for assignment in &mut plan.assignments {
if let Some(metric) = metrics.get(&assignment.node_id) {
if metric.status != NodeStatus::Active {
continue;
}
let available = metric.available_vram_gb;
let avg_layer_size = assignment.avg_vram_per_layer();
if available > 0.0 && avg_layer_size > 0.0 {
let potential_layers = (available / avg_layer_size) as usize;
if potential_layers > 2 {
needs_rebalance = true;
break;
}
}
}
}
needs_rebalance
}
#[cfg(test)]
pub fn simulate_layer_streaming(
node_id: &str,
cluster: &ClusterState,
start_layer: usize,
end_layer: usize,
) -> Option<crate::cluster::NodeMetrics> {
let mut metrics = cluster.get_metrics(node_id)?;
let num_layers = end_layer - start_layer;
let avg_vram = (metrics.available_vram_gb + metrics.vram_gb) / 2.0;
let vram_per_layer = avg_vram / num_layers as f32;
metrics.record_vram_usage(vram_per_layer * num_layers as f32);
metrics.set_streaming_layers(start_layer, end_layer);
Some(metrics)
}
pub fn calculate_cluster_health(cluster: &ClusterState) -> (f32, usize, Vec<String>) {
let metrics = cluster
.metrics
.lock()
.unwrap_or_else(|poison| poison.into_inner());
if metrics.is_empty() {
return (0.0, 0, vec![]);
}
let mut sum_delivery_ratio = 0.0f32;
let mut active_count = 0usize;
let mut failed_nodes = Vec::new();
for (node_id, metric) in metrics.iter() {
match metric.status {
NodeStatus::Active => {
sum_delivery_ratio += metric.delivery_ratio;
active_count += 1;
}
NodeStatus::Failed => {
failed_nodes.push(node_id.clone());
}
_ => {}
}
}
let avg_delivery_ratio = if active_count > 0 {
sum_delivery_ratio / active_count as f32
} else {
0.0
};
let failed_count = failed_nodes.len();
(avg_delivery_ratio, failed_count, failed_nodes)
}
#[derive(Debug, Clone)]
pub struct MigrationPlanner {
pub source_node: String,
pub target_node: String,
pub layers: Vec<usize>,
pub est_migration_time_ms: f32,
}
impl MigrationPlanner {
pub fn new(source: String, target: String, layers: Vec<usize>) -> Self {
let est_time = layers.len() as f32 * 45.0; Self {
source_node: source,
target_node: target,
layers,
est_migration_time_ms: est_time,
}
}
pub fn generate_handoff_plan(&self) -> Vec<String> {
let mut steps = Vec::new();
let target_node = &self.target_node;
let source_node = &self.source_node;
steps.push(format!(
"PREPARE: Target node {target_node} allocating VRAM"
));
steps.push(format!(
"STREAM: Moving layers {:?} to {target_node}",
self.layers
));
steps.push(format!("VERIFY: Integrity check on {target_node}"));
steps.push(format!("COMMIT: Switch routing to {target_node}"));
steps.push(format!("CLEANUP: Free VRAM on source node {source_node}"));
steps
}
}
pub struct RebalanceTrigger {
pub min_imbalance_ratio: f32,
pub max_p95_latency_ms: f32,
}
impl Default for RebalanceTrigger {
fn default() -> Self {
Self {
min_imbalance_ratio: 0.25, max_p95_latency_ms: 25.0,
}
}
}
impl RebalanceTrigger {
pub fn evaluate(
&self,
cluster: &ClusterState,
current_plan: &PlacementPlan,
) -> Option<MigrationPlanner> {
let (overtaxed, available) = {
let metrics = cluster
.metrics
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let mut active_count = 0usize;
let mut overtaxed_ref: Option<&str> = None;
let mut available_ref: Option<&str> = None;
for m in metrics.values() {
if m.status == NodeStatus::Active {
active_count += 1;
let is_overtaxed = m.delivery_ratio < 0.90 || m.avg_latency_us > 15.0;
if is_overtaxed {
if overtaxed_ref.is_none() {
overtaxed_ref = Some(&m.name);
}
} else if available_ref.is_none() && m.available_vram_gb > 8.0 {
available_ref = Some(&m.name);
}
}
}
if active_count < 2 {
return None;
}
(
overtaxed_ref.map(|s| s.to_string()),
available_ref.map(|s| s.to_string()),
)
};
if let (Some(source), Some(target)) = (overtaxed, available) {
if source == target {
return None;
}
let layers_to_move = current_plan
.assignments
.iter()
.find(|a| a.node_id == source)
.map(|a| (a.start_layer..a.end_layer).take(2).collect::<Vec<_>>())
.unwrap_or_default();
if !layers_to_move.is_empty() {
return Some(MigrationPlanner::new(source, target, layers_to_move));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::AccelerationMode;
fn sample_layers(count: usize, vram_gb: f32) -> Vec<LayerSpec> {
(0..count)
.map(|index| LayerSpec {
index,
vram_gb,
num_weights: 0,
})
.collect()
}
#[test]
fn greedily_places_layers_across_nodes() {
let nodes = vec![
NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
NodeResources::new("node-b", 12.0, 32.0, "8.6", None),
];
let assignments = assign_layers_sequentially(&nodes, &sample_layers(33, 1.0)).unwrap();
assert_eq!(
assignments,
vec![
LayerAssignment {
node_id: "node-a".into(),
start_layer: 0,
end_layer: 24,
used_vram_gb: 24.0,
num_layers: 24,
},
LayerAssignment {
node_id: "node-b".into(),
start_layer: 24,
end_layer: 33,
used_vram_gb: 9.0,
num_layers: 9,
}
]
);
}
#[test]
fn reports_insufficient_capacity() {
let nodes = vec![NodeResources::new("node-a", 2.0, 64.0, "8.9", None)];
let error = assign_layers_sequentially(&nodes, &sample_layers(3, 1.0)).unwrap_err();
assert!(error.contains("insufficient cluster VRAM"));
}
#[test]
fn selects_quantization_mode_from_delivery_ratio() {
assert_eq!(select_quantization_mode(0.98), QuantizationMode::None);
assert_eq!(select_quantization_mode(0.90), QuantizationMode::Int8);
assert_eq!(select_quantization_mode(0.75), QuantizationMode::Int4);
}
#[test]
fn placement_plan_summary() {
let plan = PlacementPlan::new(
vec![
LayerAssignment::new("node-a".into(), 0, 24, 24.0),
LayerAssignment::new("node-b".into(), 24, 33, 9.0),
],
QuantizationMode::None,
);
let summary = plan.summary();
assert!(summary.contains("Total layers: 33"));
assert!(summary.contains("Full Precision"));
}
#[test]
fn rebalance_assignments_detects_capacity() {
let mut plan = PlacementPlan::new(
vec![LayerAssignment::new("node-a".into(), 0, 24, 24.0)],
QuantizationMode::None,
);
assert!(!rebalance_assignments(&ClusterState::default(), &mut plan));
}
#[test]
fn runtime_profile_chunks_large_assignments() {
let assignments = vec![LayerAssignment::new("node-a".into(), 0, 12, 12.0)];
let chunked = chunk_assignments_for_workers(assignments, 5);
assert_eq!(chunked.len(), 3);
assert_eq!(chunked[0].start_layer, 0);
assert_eq!(chunked[0].end_layer, 5);
assert_eq!(chunked[2].start_layer, 10);
assert_eq!(chunked[2].end_layer, 12);
}
#[test]
fn runtime_profile_assignment_keeps_trailing_zero_vram_layers() {
let nodes = vec![NodeResources::new("node-a", 24.0, 64.0, "8.9", None)];
let layers = sample_layers(3, 0.0);
let profile = RuntimeProfile {
node_resources: NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
logical_cores: 4,
recommended_workers: 1,
acceleration_mode: AccelerationMode::Generic,
gpu_backend: crate::host::GpuBackend::Cpu,
xdp_supported: false,
detection_source: String::from("test"),
probe_mode: crate::host::ProbeMode::Fast,
};
let assignments = assign_layers_with_runtime_profile(&nodes, &layers, &profile).unwrap();
let total: usize = assignments.iter().map(|a| a.num_layers).sum();
assert_eq!(total, 3, "all zero-VRAM layers must still be assigned");
}
#[test]
fn fault_tolerance_handles_all_failed_nodes_without_panicking() {
let cluster = ClusterState::new();
cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
cluster.mark_failed("node-a");
let plan = assign_layers_with_fault_tolerance(&cluster, &sample_layers(4, 1.0)).unwrap();
assert_eq!(plan.quantization_mode, QuantizationMode::Int4);
}
#[test]
fn planning_tuning_scales_chunk_size_with_workers() {
let profile = RuntimeProfile {
node_resources: NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
logical_cores: 16,
recommended_workers: 8,
acceleration_mode: AccelerationMode::Gpu,
gpu_backend: crate::host::GpuBackend::Cuda,
xdp_supported: true,
detection_source: String::from("test"),
probe_mode: crate::host::ProbeMode::Fast,
};
let tuning = PlanningTuning::from_runtime_profile(&profile, 40);
assert!(tuning.max_layers_per_assignment <= 4);
}
#[test]
fn test_rebalance_trigger_evaluate_never_selects_overtaxed_as_available() {
let cluster = ClusterState::new();
cluster.register(NodeResources::new("node-1", 24.0, 64.0, "8.9", None));
cluster.register(NodeResources::new("node-2", 24.0, 64.0, "8.9", None));
cluster.get_metrics_mut("node-1", |m| {
m.delivery_ratio = 0.50;
m.available_vram_gb = 16.0;
});
cluster.get_metrics_mut("node-2", |m| {
m.delivery_ratio = 0.60;
m.available_vram_gb = 16.0;
});
let trigger = RebalanceTrigger::default();
let plan = PlacementPlan::new(
vec![LayerAssignment::new("node-1".into(), 0, 10, 10.0)],
QuantizationMode::None,
);
let planner = trigger.evaluate(&cluster, &plan);
assert!(
planner.is_none(),
"overtaxed nodes must never be selected as available rebalance targets"
);
}
}