Run the full zero-config master setup. Discovers workers via mDNS, computes layer assignments based on VRAM, connects to each worker with mutual authentication, pushes model data as needed, and returns a `Topology` ready for normal inference.
(
cluster_key: &str,
model_path: &Path,
discovery_timeout: Duration,
min_workers: usize,
)
| 160 | /// connects to each worker with mutual authentication, pushes model data |
| 161 | /// as needed, and returns a `Topology` ready for normal inference. |
| 162 | pub async fn master_setup( |
| 163 | cluster_key: &str, |
| 164 | model_path: &Path, |
| 165 | discovery_timeout: Duration, |
| 166 | min_workers: usize, |
| 167 | ) -> Result<Topology> { |
| 168 | // Read config.json and compute a fingerprint for cache keying |
| 169 | let config_path = model_path.join("config.json"); |
| 170 | let config_data = std::fs::read_to_string(&config_path) |
| 171 | .map_err(|e| anyhow!("failed to read {}: {}", config_path.display(), e))?; |
| 172 | let model_hash = { |
| 173 | use sha2::{Digest, Sha256}; |
| 174 | let mut hasher = Sha256::new(); |
| 175 | hasher.update(config_data.as_bytes()); |
| 176 | let result = hasher.finalize(); |
| 177 | hex::encode(&result[..4]) |
| 178 | }; |
| 179 | let config_json: serde_json::Value = serde_json::from_str(&config_data)?; |
| 180 | let num_layers = config_json |
| 181 | .get("num_hidden_layers") |
| 182 | .and_then(|v| v.as_u64()) |
| 183 | .or_else(|| { |
| 184 | // Some models (e.g. Qwen3.5) nest config under text_config |
| 185 | config_json |
| 186 | .get("text_config") |
| 187 | .and_then(|tc| tc.get("num_hidden_layers")) |
| 188 | .and_then(|v| v.as_u64()) |
| 189 | }) |
| 190 | .ok_or_else(|| anyhow!("num_hidden_layers not found in config.json"))? as usize; |
| 191 | |
| 192 | // Derive layer naming prefix from architecture (needed early for layer size estimation) |
| 193 | let layer_prefix = default::layer_prefix_for_config(&config_json); |
| 194 | |
| 195 | log::info!( |
| 196 | "model has {} transformer layers (prefix: {})", |
| 197 | num_layers, |
| 198 | &layer_prefix, |
| 199 | ); |
| 200 | |
| 201 | // Detect master GPU and free VRAM concurrently with the discovery window |
| 202 | // (nvidia-smi can take ~1-2s; hide that cost inside the discovery timeout). |
| 203 | let master_gpus = discovery::detect_gpus(); |
| 204 | let master_tflops: f64 = master_gpus.iter().map(|g| g.tflops as f64).sum(); |
| 205 | let free_gpu_fut = tokio::task::spawn_blocking(detect_free_gpu_memory); |
| 206 | |
| 207 | // Discover workers |
| 208 | let workers = discovery::discover_workers(cluster_key, discovery_timeout, min_workers).await?; |
| 209 | if workers.is_empty() { |
| 210 | log::warn!("no workers discovered — all layers will be loaded locally"); |
| 211 | return Ok(Topology::new()); |
| 212 | } |
| 213 | |
| 214 | // nvidia-smi result is now ready (ran during the discovery window) |
| 215 | let master_free_from_smi = free_gpu_fut.await.unwrap_or(0); |
| 216 | |
| 217 | // Estimate per-layer size for VRAM-aware capping. |
| 218 | // Uses weight_map tensor-count fractions to exclude non-layer weights |
| 219 | // (visual encoder, MTP heads, embeddings, lm_head, FP8 scale_inv, etc.). |
no test coverage detected