|
|
@@ -40,6 +40,10 @@ pub struct ServeOpts {
|
|
|
/// of the shared HF cache. `Some("")` = bare `--mirror` → default
|
|
|
/// `~/.cache/hf-models`; `Some(path)` = that exact path; `None` = off.
|
|
|
pub mirror: Option<String>,
|
|
|
+ /// CLI override for recipe scaling: `Some("")` = bare `--fit` → scale to
|
|
|
+ /// the whole selected cluster; `Some("N")` = scale to N nodes;
|
|
|
+ /// `None` = unset. See `docker::serve`.
|
|
|
+ pub fit: Option<String>,
|
|
|
}
|
|
|
/// Shell-quote a string for safe interpolation into a remote `sh -c`
|
|
|
/// command (single-quote wrapping, embedded quotes escaped). Re-exported
|
|
|
@@ -53,13 +57,41 @@ pub async fn serve(
|
|
|
node_name: Option<&str>,
|
|
|
opts: ServeOpts,
|
|
|
) -> Result<Cluster> {
|
|
|
- let recipe = load_recipe(recipe_name).await?;
|
|
|
+ let mut recipe = load_recipe(recipe_name).await?;
|
|
|
let rn = recipe.name.clone();
|
|
|
let re = recipe.engine.clone();
|
|
|
info!(
|
|
|
"{}",
|
|
|
fluent_i18n::t!("recipe-starting", { "name" => rn, "engine" => re, "backend" => recipe.distributed_backend.map_or("none".to_string(), |b| b.to_string()) })
|
|
|
);
|
|
|
+ // `--fit`: override required_nodes + tensor_parallel_size so TP tracks
|
|
|
+ // the node count (the recipe's `required_nodes == Some(tp_size)` path in
|
|
|
+ // `start_cluster` makes tp_size follow nnodes). `pipeline_parallel_size`
|
|
|
+ // is left untouched. Bare `--fit` (whole cluster) is handled after
|
|
|
+ // cluster selection, when nnodes is known; for now just clear the cap so
|
|
|
+ // selection/trim keeps every cluster node.
|
|
|
+ //
|
|
|
+ // Mutually exclusive with `--solo` (solo = 1 node, no dist flags).
|
|
|
+ let fit: Option<u32> = match opts.fit.as_deref() {
|
|
|
+ None => None,
|
|
|
+ Some("") => Some(0),
|
|
|
+ Some(s) => Some(
|
|
|
+ s.parse()
|
|
|
+ .context(format!("--fit expects a node count, got '{}'", s))?,
|
|
|
+ ),
|
|
|
+ };
|
|
|
+ if fit.is_some() && opts.solo.is_some() {
|
|
|
+ anyhow::bail!("--fit and --solo are mutually exclusive");
|
|
|
+ }
|
|
|
+ match fit {
|
|
|
+ Some(0) => recipe.required_nodes = None,
|
|
|
+ Some(n) => {
|
|
|
+ recipe.required_nodes = Some(n);
|
|
|
+ recipe.tensor_parallel_size = n;
|
|
|
+ info!("--fit: scaling to {} nodes (tp = {})", n, n);
|
|
|
+ }
|
|
|
+ None => {}
|
|
|
+ }
|
|
|
// Prepare cluster: probe, auto-power-on if requested, select.
|
|
|
use crate::ends::{SshNodePinger, SshProber, ha_controller_from_config};
|
|
|
use crate::orchestrate::{BootTiming, PrepareRequest, prepare_cluster};
|
|
|
@@ -123,6 +155,19 @@ pub async fn serve(
|
|
|
);
|
|
|
cluster.nodes = vec![solo_node];
|
|
|
}
|
|
|
+ // Bare `--fit` (whole cluster): now that `cluster.nodes` is resolved, fix
|
|
|
+ // tp to the node count so `start_cluster`'s `tp_size` auto-track fires
|
|
|
+ // (required_nodes == Some(tp_size) → tp_size = nnodes). required_nodes is
|
|
|
+ // already None (no trim), so every node is kept.
|
|
|
+ if fit == Some(0) {
|
|
|
+ let nn = cluster.nodes.len() as u32;
|
|
|
+ recipe.required_nodes = Some(nn);
|
|
|
+ recipe.tensor_parallel_size = nn;
|
|
|
+ info!(
|
|
|
+ "--fit: scaling to whole cluster '{}' ({} nodes, tp = {})",
|
|
|
+ cluster.name, nn, nn
|
|
|
+ );
|
|
|
+ }
|
|
|
crate::before_serve::run(
|
|
|
config,
|
|
|
&cluster,
|
|
|
@@ -170,7 +215,6 @@ pub async fn serve(
|
|
|
)
|
|
|
.await?;
|
|
|
let image = try_repaired_image(config, &cluster, &recipe).await?;
|
|
|
- let mut recipe = recipe;
|
|
|
if image != recipe.docker_image {
|
|
|
recipe.docker_image = image.clone();
|
|
|
}
|