Jelajahi Sumber

engine: gr8 driver (Phase 2) — --tp/--peers rendezvous, --model, fixed API port

Gr8Driver: model via --model, rendezvous via --tp/--peers (mgmt-LAN IPs in
rank order, threaded into EngineFlagsCtx from cluster.nodes), no headless,
no bind-IP env, takes_host_port=false (gr8 API fixed at :8100). validate
port-convention now exempts engines whose driver doesn't take --host/--port.
Dry-run renders the exact gr8 glm53 up contract; make check green.
Alexander Korolev 1 Minggu lalu
induk
melakukan
f772f3d5e6
5 mengubah file dengan 112 tambahan dan 5 penghapusan
  1. 11 0
      src/docker/command.rs
  2. 9 4
      src/docker/tail.rs
  3. 81 0
      src/engine.rs
  4. 8 0
      src/tmux/commands.rs
  5. 3 1
      src/validate.rs

+ 11 - 0
src/docker/command.rs

@@ -363,6 +363,16 @@ pub(crate) fn build_docker_command(
         ));
     }
     let master_ip = ctx.cluster.effective_master_ip();
+    // gr8's `--peers`: the launch-node mgmt-LAN IPs in rank order (each rank
+    // finds itself in the list). vLLM/SGLang ignore this.
+    let mut peers: Vec<(u32, String)> = ctx
+        .cluster
+        .nodes
+        .iter()
+        .map(|n| (n.rank, n.ssh_ip.clone()))
+        .collect();
+    peers.sort_by_key(|(r, _)| *r);
+    let peer_ips: Vec<String> = peers.into_iter().map(|(_, ip)| ip).collect();
     let tctx = tail::TailCtx {
         recipe: &ctx.recipe,
         tp_size: ctx.tp_size,
@@ -370,6 +380,7 @@ pub(crate) fn build_docker_command(
         nnodes: ctx.nnodes,
         init_port: ctx.init_port,
         master_ib_ip: &master_ip,
+        peers: &peer_ips,
     };
     let tail = tail::build_tail(&tctx, node);
     let img = shq(&ctx.recipe.docker_image);

+ 9 - 4
src/docker/tail.rs

@@ -12,6 +12,7 @@ pub(crate) struct TailCtx<'a> {
     pub nnodes: u32,
     pub init_port: u16,
     pub master_ib_ip: &'a str,
+    pub peers: &'a [String],
 }
 
 /// Build the docker command tail — everything after the image name.
@@ -33,12 +34,16 @@ pub fn build_tail(ctx: &TailCtx<'_>, node: &Node) -> Vec<String> {
         nnodes: ctx.nnodes,
         init_port: ctx.init_port,
         master_ib_ip: ctx.master_ib_ip,
+        peers: ctx.peers,
     };
     tail.extend(build_engine_args(&ectx, node));
 
-    // Docker always binds --host 0.0.0.0 and --port.
-    tail.push("--host".to_string());
-    tail.push("0.0.0.0".to_string());
-    tail.extend(["--port".into(), ctx.recipe.port.to_string()]);
+    // Engines that bind a configurable port (vLLM/SGLang) get `--host
+    // 0.0.0.0 --port`; engines with a fixed API port (gr8, DGPP) do not.
+    if crate::engine::driver_for(&ctx.recipe.engine).takes_host_port() {
+        tail.push("--host".to_string());
+        tail.push("0.0.0.0".to_string());
+        tail.extend(["--port".into(), ctx.recipe.port.to_string()]);
+    }
     tail
 }

+ 81 - 0
src/engine.rs

@@ -24,6 +24,9 @@ pub struct EngineFlagsCtx<'a> {
     pub nnodes: u32,
     pub init_port: u16,
     pub master_ib_ip: &'a str,
+    /// Ordered launch-node IPs (rank order). gr8's `--peers` list; vLLM/SGLang
+    /// ignore it.
+    pub peers: &'a [String],
 }
 
 /// Per-engine launch semantics. See the module docs.
@@ -60,6 +63,13 @@ pub trait EngineDriver: Send + Sync {
         false
     }
 
+    /// Whether the deployment path appends `--host 0.0.0.0 --port <port>` to
+    /// the engine command. vLLM/SGLang take them; engines with a fixed/built-in
+    /// API port (gr8 :8100, DGPP :18080) do not.
+    fn takes_host_port(&self) -> bool {
+        true
+    }
+
     /// tmux entrypoint fallback when the recipe sets none.
     fn entrypoint_default(&self) -> Vec<String>;
 
@@ -156,12 +166,47 @@ impl EngineDriver for SglangDriver {
     }
 }
 
+/// gr8 (`kindlingai/gr8`): a from-scratch CUDA engine for full GLM-5.3,
+/// tensor-parallel across 3/4/6 Sparks. Per-node launch (every box runs the
+/// same `gr8 glm53 up` command; each rank finds itself in `--peers`). Model
+/// via `--model`, rendezvous via `--tp`/`--peers` (mgmt-LAN IPs), no headless,
+/// no bind-IP env, fixed API port :8100 (no `--host`/`--port`).
+pub struct Gr8Driver;
+
+impl EngineDriver for Gr8Driver {
+    fn model_args(&self, recipe: &Recipe) -> Vec<String> {
+        if recipe.model.is_empty() {
+            Vec::new()
+        } else {
+            vec!["--model".into(), recipe.model.clone()]
+        }
+    }
+
+    fn distributed_args(&self, ctx: &EngineFlagsCtx<'_>, _node: &Node) -> Vec<String> {
+        vec![
+            "--tp".into(),
+            ctx.tp_size.to_string(),
+            "--peers".into(),
+            ctx.peers.join(","),
+        ]
+    }
+
+    fn takes_host_port(&self) -> bool {
+        false
+    }
+
+    fn entrypoint_default(&self) -> Vec<String> {
+        vec!["/opt/gr8/gr8".into(), "glm53".into(), "up".into()]
+    }
+}
+
 /// Resolve the driver for a recipe's `engine` string. Unknown engines fall
 /// back to the vLLM driver (matching the historical default: an engine with no
 /// dedicated driver still gets the vLLM-style tail).
 pub fn driver_for(engine: &str) -> &'static dyn EngineDriver {
     match engine {
         "sglang" => &SglangDriver,
+        "gr8" => &Gr8Driver,
         "vllm" => &VllmDriver,
         _ => &VllmDriver,
     }
@@ -270,6 +315,24 @@ mod tests {
             nnodes,
             init_port: 25000,
             master_ib_ip: "10.0.0.1",
+            peers: &[],
+        }
+    }
+
+    fn ctx_peers<'a>(
+        r: &'a Recipe,
+        tp: u32,
+        nnodes: u32,
+        peers: &'a [String],
+    ) -> EngineFlagsCtx<'a> {
+        EngineFlagsCtx {
+            recipe: r,
+            tp_size: tp,
+            pp_size: 1,
+            nnodes,
+            init_port: 25000,
+            master_ib_ip: "10.0.0.1",
+            peers,
         }
     }
 
@@ -335,4 +398,22 @@ mod tests {
         // Unknown engines fall back to the vLLM driver (historical default).
         assert!(driver_for("something-new").headless_workers());
     }
+
+    #[test]
+    fn gr8_uses_tp_peers_and_model_flag() {
+        let mut r = base_recipe();
+        r.engine = "gr8".into();
+        r.entrypoint = vec!["/opt/gr8/gr8".into(), "glm53".into(), "up".into()];
+        let peers = vec!["10.0.0.1".to_string(), "10.0.0.2".to_string()];
+        let c = ctx_peers(&r, 2, 2, &peers);
+        let args = build_engine_args(&c, &tnode(0));
+        // model via --model, rendezvous via --tp/--peers, no --headless.
+        assert!(args.contains(&"--model".into()));
+        assert!(args.contains(&"--tp".into()));
+        assert!(args.contains(&"--peers".into()));
+        assert!(args.contains(&"10.0.0.1,10.0.0.2".into()));
+        assert!(!args.contains(&"--headless".into()));
+        // gr8 does not take --host/--port (fixed API port).
+        assert!(!driver_for("gr8").takes_host_port());
+    }
 }

+ 8 - 0
src/tmux/commands.rs

@@ -40,6 +40,13 @@ impl TmuxManager {
 
         // Shared engine args: model, recipe args, distributed flags, headless.
         let master_ip = cluster.effective_master_ip();
+        let mut peer_pairs: Vec<(u32, String)> = cluster
+            .nodes
+            .iter()
+            .map(|n| (n.rank, n.ssh_ip.clone()))
+            .collect();
+        peer_pairs.sort_by_key(|(r, _)| *r);
+        let peer_ips: Vec<String> = peer_pairs.into_iter().map(|(_, ip)| ip).collect();
         let ectx = EngineFlagsCtx {
             recipe,
             tp_size: recipe.tensor_parallel_size,
@@ -47,6 +54,7 @@ impl TmuxManager {
             nnodes,
             init_port: 25000,
             master_ib_ip: &master_ip,
+            peers: &peer_ips,
         };
         parts.extend(build_engine_args(&ectx, node));
 

+ 3 - 1
src/validate.rs

@@ -54,7 +54,9 @@ fn check_recipe(name: &str, r: &Recipe, images_dirs: &[&Path], out: &mut Vec<Vio
         });
     }
     // Port convention: every recipe serves on the cluster default port.
-    if r.port != 30000 {
+    // Engines with a fixed, non-configurable API port (gr8 :8100, DGPP
+    // :18080 — drivers that don't take --host/--port) are exempt.
+    if r.port != 30000 && crate::engine::driver_for(&r.engine).takes_host_port() {
         out.push(Violation {
             recipe: name.to_string(),
             level: Level::Error,