|
|
@@ -25,8 +25,13 @@
|
|
|
use anyhow::{Result, bail};
|
|
|
use tracing::info;
|
|
|
|
|
|
-use crate::config::{Config, Node, Recipe};
|
|
|
+use crate::config::{Config, Recipe};
|
|
|
use crate::docker::{ServeOpts, shq};
|
|
|
+use crate::native::flare_orbit::{controller_for, extra_flags_for};
|
|
|
+// Re-exported: the driver is the FLARE entry point the rest of the launcher uses.
|
|
|
+pub use crate::native::flare_orbit::{
|
|
|
+ local_dir_for, orbit_cluster_for, orbit_cluster_paired, orbit_node_name,
|
|
|
+};
|
|
|
|
|
|
/// FLARE API port on the controller (recipe `[flare].http_port`, else the
|
|
|
/// cluster default 30000 — the same port every other lane serves on).
|
|
|
@@ -38,101 +43,6 @@ pub fn http_port(recipe: &Recipe) -> u16 {
|
|
|
.unwrap_or(30000)
|
|
|
}
|
|
|
|
|
|
-/// Orbit cluster name for one of OUR clusters: the per-cluster mapping wins,
|
|
|
-/// then the flat `[flare].orbit_cluster`, then the default. This is what makes
|
|
|
-/// one launcher config work on `dgx58-switch` and `prod4` alike.
|
|
|
-pub fn orbit_cluster_for(config: &Config, cluster: &str) -> String {
|
|
|
- let f = config.flare.as_ref();
|
|
|
- f.and_then(|f| f.clusters.get(cluster))
|
|
|
- .and_then(|c| c.orbit_cluster.clone())
|
|
|
- .or_else(|| f.and_then(|f| f.orbit_cluster.clone()))
|
|
|
- .unwrap_or_else(|| "sparks".to_string())
|
|
|
-}
|
|
|
-
|
|
|
-/// The orbit cluster a **specific** cluster is paired with, or `None` when the
|
|
|
-/// config does not say (the flat `[flare].orbit_cluster` is then only a
|
|
|
-/// fallback for `serve`, not evidence of a pairing).
|
|
|
-///
|
|
|
-/// This is the identification rule the dashboard and `health` use: a cluster
|
|
|
-/// whose own nodes were never paired with an orbit must not be probed with the
|
|
|
-/// flat default (`sparks`) — `6nodes-switch`, `1node-*`, `ds4` and `qwen38`
|
|
|
-/// share nodes with the paired clusters, so asking one of them `--cluster
|
|
|
-/// sparks` made the sparks lane look like a lane of *that* cluster.
|
|
|
-pub fn orbit_cluster_paired(config: &Config, cluster: &str) -> Option<String> {
|
|
|
- let f = config.flare.as_ref()?;
|
|
|
- if let Some(own) = f.clusters.get(cluster) {
|
|
|
- // A per-cluster block without an orbit name inherits the flat one —
|
|
|
- // the operator already said "this cluster speaks FLARE".
|
|
|
- return own
|
|
|
- .orbit_cluster
|
|
|
- .clone()
|
|
|
- .or_else(|| f.orbit_cluster.clone());
|
|
|
- }
|
|
|
- // Once the map exists it is the source of truth: the flat block describes
|
|
|
- // the machine it was written for, and our clusters share nodes (dgx5 is
|
|
|
- // rank 0 of `dgx58-switch` AND rank 4 of `6nodes-switch`), so a flat
|
|
|
- // `sparks` would pair `6nodes-switch` too.
|
|
|
- if !f.clusters.is_empty() {
|
|
|
- return None;
|
|
|
- }
|
|
|
- let flat = f.orbit_cluster.clone()?;
|
|
|
- match f.controller_node.as_deref() {
|
|
|
- // Legacy flat block: it speaks for the cluster that holds its
|
|
|
- // controller node.
|
|
|
- Some(ctl) => config
|
|
|
- .clusters
|
|
|
- .iter()
|
|
|
- .find(|c| c.name == cluster)
|
|
|
- .is_some_and(|c| c.nodes.iter().any(|n| n.name == ctl))
|
|
|
- .then_some(flat),
|
|
|
- // No controller either: unambiguous only with a single cluster.
|
|
|
- None => (config.clusters.len() == 1 && config.clusters[0].name == cluster).then_some(flat),
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Extra `orbit run` flags for a cluster (per-cluster map first, then flat).
|
|
|
-/// Fabric shape is a cluster property. The prod 4-node switch uses no extra
|
|
|
-/// flag: FLARE finds the star links itself once the legacy CX7 ports are down;
|
|
|
-/// `--roce-ring` was a symptom-level fix and is retired.
|
|
|
-pub fn extra_flags_for(config: &Config, cluster: &str) -> Vec<String> {
|
|
|
- let Some(f) = config.flare.as_ref() else {
|
|
|
- return Vec::new();
|
|
|
- };
|
|
|
- let per = f
|
|
|
- .clusters
|
|
|
- .get(cluster)
|
|
|
- .map(|c| c.extra_flags.clone())
|
|
|
- .unwrap_or_default();
|
|
|
- if per.is_empty() {
|
|
|
- f.extra_flags.clone()
|
|
|
- } else {
|
|
|
- per
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Local package mirror dir for a cluster (per-cluster map first).
|
|
|
-pub(crate) fn local_dir_for(config: &Config, cluster: &str) -> Option<String> {
|
|
|
- let f = config.flare.as_ref()?;
|
|
|
- f.clusters
|
|
|
- .get(cluster)
|
|
|
- .and_then(|c| c.local_dir.clone())
|
|
|
- .or_else(|| f.local_dir.clone())
|
|
|
-}
|
|
|
-
|
|
|
-/// The orbit-side name for one of our nodes (rank order → `orbit_nodes`,
|
|
|
-/// falling back to the cluster's own name).
|
|
|
-pub(crate) fn orbit_node_name(config: &Config, cluster: &str, node: &Node) -> String {
|
|
|
- let f = config.flare.as_ref();
|
|
|
- let per_cluster = f
|
|
|
- .and_then(|f| f.clusters.get(cluster))
|
|
|
- .map(|c| c.orbit_nodes.as_slice())
|
|
|
- .filter(|v| !v.is_empty());
|
|
|
- per_cluster
|
|
|
- .or_else(|| f.map(|f| f.orbit_nodes.as_slice()))
|
|
|
- .and_then(|v| v.get(node.rank as usize).cloned())
|
|
|
- .unwrap_or_else(|| node.name.clone())
|
|
|
-}
|
|
|
-
|
|
|
pub(crate) fn binary(config: &Config) -> String {
|
|
|
config
|
|
|
.flare
|
|
|
@@ -325,14 +235,6 @@ pub fn parse_package_path(stdout: &str) -> Option<String> {
|
|
|
.map(str::to_string)
|
|
|
}
|
|
|
|
|
|
-fn head(cluster: &crate::config::Cluster) -> Result<&Node> {
|
|
|
- cluster
|
|
|
- .nodes
|
|
|
- .iter()
|
|
|
- .find(|n| n.rank == 0)
|
|
|
- .ok_or_else(|| anyhow::anyhow!("no head node (rank 0) in cluster"))
|
|
|
-}
|
|
|
-
|
|
|
/// Launch a FLARE recipe: preflight, package download on the controller, then
|
|
|
/// `orbit run` detached. Readiness (`/health`, `/v1/models`) is polled by the
|
|
|
/// caller, like every other engine.
|
|
|
@@ -342,7 +244,7 @@ pub async fn serve(
|
|
|
cluster: &crate::config::Cluster,
|
|
|
dry_run: bool,
|
|
|
) -> Result<()> {
|
|
|
- let controller = head(cluster)?;
|
|
|
+ let controller = controller_for(config, cluster)?;
|
|
|
let orbit = orbit_cluster_for(config, &cluster.name);
|
|
|
if dry_run {
|
|
|
println!(
|
|
|
@@ -422,7 +324,11 @@ pub async fn serve(
|
|
|
// Dry-run: print the download and mirror commands, no SSH.
|
|
|
let dl = download_cmd(config, recipe)?;
|
|
|
package = crate::ssh::run_ssh_long_running(config, controller, dl, true).await?;
|
|
|
- package = parse_package_path(&package).unwrap_or_default();
|
|
|
+ // The path is printed by a real download; in a dry run there is none,
|
|
|
+ // so show the shape (a store-relative `models/<version>/model.safetensors`,
|
|
|
+ // which is what the mirror shape depends on).
|
|
|
+ package = parse_package_path(&package)
|
|
|
+ .unwrap_or_else(|| "<package-store>/models/<version>/model.safetensors".into());
|
|
|
if let Some(dir) = local_dir_for(config, &cluster.name) {
|
|
|
let dir = dir.replace('~', "$HOME");
|
|
|
let cmd = mirror_cmd(&dir, &package);
|
|
|
@@ -475,7 +381,9 @@ pub async fn stop(
|
|
|
cluster: &crate::config::Cluster,
|
|
|
dry_run: bool,
|
|
|
) -> Result<()> {
|
|
|
- let controller = head(cluster)?;
|
|
|
+ // `orbit stop` must reach the orbit's controller, which the config may pin
|
|
|
+ // to a node other than our rank 0.
|
|
|
+ let controller = controller_for(config, cluster)?;
|
|
|
let cmd = stop_cmd(config, recipe, &orbit_cluster_for(config, &cluster.name))?;
|
|
|
crate::ssh::run_ssh(config, controller, cmd, dry_run).await?;
|
|
|
Ok(())
|