use anyhow::{Context, Result};
use rlx_runtime::{Device, parse_device_list};
use serde::{Deserialize, Serialize};
use std::ops::Range;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum KvPolicy {
#[default]
None,
Host,
Device,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PlacementPolicy {
#[default]
RamBalanced,
Throughput,
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub addr: String,
#[serde(default)]
pub ssh: Option<String>,
pub ckpt_dir: String,
#[serde(default = "default_device")]
pub device: String,
#[serde(default = "default_precision")]
pub precision: String,
#[serde(default)]
pub kv_cache: KvPolicy,
#[serde(default)]
pub rng_seed: Option<u64>,
#[serde(default)]
pub max_ram_gb: Option<f64>,
#[serde(default)]
pub layers: Option<String>,
}
fn default_device() -> String {
"cpu".into()
}
fn default_precision() -> String {
"bf16".into()
}
impl NodeConfig {
pub fn devices(&self) -> Result<Vec<Device>> {
parse_device_list(&self.device.replace('+', ","))
.map_err(|e| anyhow::anyhow!("node {}: bad device `{}`: {e:?}", self.addr, self.device))
}
pub fn primary_device(&self) -> Device {
self.devices()
.ok()
.and_then(|d| d.into_iter().next())
.unwrap_or(Device::Cpu)
}
pub fn cpu_offload(&self) -> bool {
self.devices()
.map(|d| d.len() > 1 && d.contains(&Device::Cpu))
.unwrap_or(false)
}
pub fn manual_range(&self) -> Option<Range<usize>> {
let s = self.layers.as_ref()?;
let (a, b) = s.split_once(':')?;
Some(a.trim().parse().ok()?..b.trim().parse().ok()?)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterConfig {
pub model: String,
#[serde(default = "default_seq")]
pub seq: usize,
#[serde(default)]
pub rng_seed: Option<u64>,
#[serde(default = "default_reserve")]
pub reserve_ram_gb: f64,
#[serde(default)]
pub placement: PlacementSection,
#[serde(rename = "node")]
pub nodes: Vec<NodeConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlacementSection {
#[serde(default)]
pub policy: PlacementPolicy,
}
fn default_seq() -> usize {
8
}
fn default_reserve() -> f64 {
6.0
}
impl ClusterConfig {
pub fn from_toml_str(s: &str) -> Result<Self> {
toml::from_str(s).context("parse cluster TOML")
}
pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
let s = std::fs::read_to_string(p.as_ref())
.with_context(|| format!("read {}", p.as_ref().display()))?;
Self::from_toml_str(&s)
}
pub fn seed_for(&self, i: usize) -> u64 {
self.nodes
.get(i)
.and_then(|n| n.rng_seed)
.or(self.rng_seed)
.unwrap_or(0)
}
}