@@ -360,10 +360,25 @@ pub fn run(channel: &str, keep: usize, prewarm_dir: &Path) -> Result<()> {
360360 }
361361 Decision :: Reuse { reason } => {
362362 println ! ( "pre-warm: reusing the shared {channel} runtime ({reason})" ) ;
363- return Ok ( ( ) ) ;
364363 }
365364 }
366365
366+ // Unconditional, and deliberately BEFORE the reuse early return below. The
367+ // runtime and the serving engine are installed separately: `install sdk`
368+ // lays down the ROCm runtime, `engines install` builds the engine venv
369+ // against it. `decide` only ever reasons about the runtime, so a tree whose
370+ // runtime is current but whose engine was never installed — or was left
371+ // behind with an older runtime — resolves to `Reuse`, which used to return
372+ // here having done nothing. The shared tree then served every GPU scenario a
373+ // runtime with no engine, which is the one thing those lanes exist to
374+ // exercise. Re-checking a warm tree is cheap: without `--reinstall`,
375+ // `engines install` on a ready engine installs nothing.
376+ ensure_default_engine ( & rocm, prewarm_dir) ?;
377+
378+ if !runtime_changed ( & decision) {
379+ return Ok ( ( ) ) ;
380+ }
381+
367382 // An install/update that exits 0 without leaving a registry behind is the
368383 // confusing case the lanes used to call out by hand: every scenario then falls
369384 // back to installing its own runtime and the job quietly blows its time cap.
@@ -394,6 +409,59 @@ pub fn run(channel: &str, keep: usize, prewarm_dir: &Path) -> Result<()> {
394409 Ok ( ( ) )
395410}
396411
412+ /// Whether `decision` put a new runtime in the tree, and so whether the registry
413+ /// check and the retention prune at the end of [`run`] have anything to do.
414+ ///
415+ /// Read AFTER the engine check, never inside the decision's own match arm: the
416+ /// engine is installed separately from the runtime, so every decision — reuse
417+ /// most of all, since it is the one a warm runner takes every time — has to
418+ /// reach that check before this can end the pre-warm early.
419+ const fn runtime_changed ( decision : & Decision ) -> bool {
420+ !matches ! ( decision, Decision :: Reuse { .. } )
421+ }
422+
423+ /// Install the engine the active runtime would serve on, so the shared tree has
424+ /// one before a scenario asks it to serve.
425+ ///
426+ /// Which engine that is comes from the CLI rather than from a constant here:
427+ /// `rocm engines list` marks the engine `serve` picks for the detected GPU with
428+ /// `* ` (vLLM on Instinct, Lemonade on Strix), and the pre-warm must agree with
429+ /// `serve` on every runner without this file learning the hardware map.
430+ ///
431+ /// Fatal on failure, like [`repair_poisoned_runtimes`] and unlike the freshness
432+ /// path: reusing a stale-but-working runtime keeps a lane meaningful, whereas
433+ /// serving with no engine fails every GPU scenario later and for reasons that
434+ /// name none of this.
435+ fn ensure_default_engine ( rocm : & Path , prewarm_dir : & Path ) -> Result < ( ) > {
436+ let output = rocm_command ( rocm, prewarm_dir)
437+ . args ( [ "engines" , "list" ] )
438+ . output ( )
439+ . context ( "failed to run `rocm engines list`" ) ?;
440+ if !output. status . success ( ) {
441+ bail ! ( "`rocm engines list` exited with {}" , output. status) ;
442+ }
443+ let inventory = String :: from_utf8_lossy ( & output. stdout ) ;
444+ let engine = default_engine_from_inventory ( & inventory)
445+ . context ( "`rocm engines list` did not identify a default engine" ) ?;
446+ println ! ( "pre-warm: ensuring the {engine} engine is installed" ) ;
447+ rocm_command ( rocm, prewarm_dir)
448+ . args ( [ "engines" , "install" , engine, "--yes" ] )
449+ . status_ok ( "rocm engines install" )
450+ }
451+
452+ /// The engine `rocm engines list` marks as the default for this host, if any.
453+ ///
454+ /// The inventory renders one line per engine as `{marker} {name:10} {note}` with
455+ /// the marker in column 0, then indents that engine's detail lines (` adapter:
456+ /// …`, ` runtime: …`) beneath it. Matching `* ` at the start of the line
457+ /// unindented is therefore what separates the default engine's own line from
458+ /// everything else the report prints.
459+ fn default_engine_from_inventory ( inventory : & str ) -> Option < & str > {
460+ inventory
461+ . lines ( )
462+ . find_map ( |line| line. strip_prefix ( "* " ) ?. split_whitespace ( ) . next ( ) )
463+ }
464+
397465/// Drop any managed runtime in the shared tree that records an install root
398466/// outside it, so the pre-warm reinstalls instead of serving a dead one.
399467///
@@ -731,6 +799,73 @@ mode=managed status=ready\n install_root: /tmp/rocm-e2e-XXXX/data/runtimes/
731799 assert_eq ! ( poisoned[ 0 ] . format, "tarball" ) ;
732800 }
733801
802+ /// A real `rocm engines list` on an Instinct host, captured verbatim: the
803+ /// default engine's line carries the `* ` marker in column 0, and its own
804+ /// detail lines are indented beneath it.
805+ const ENGINES_READY : & str = "\
806+ Local model engines
807+ Built-in engines are included with rocm-cli. External plugins are optional.
808+ ROCm GPU execution is required.
809+ Plugin folders:
810+ 1. /w/e2e-prewarm/data/engines/plugins (primary)
811+ lemonade default embedded Lemonade server with ROCm llama.cpp backend
812+ adapter: built-in
813+ runtime: not found
814+ * vllm Linux/WSL ROCm GPU serving engine through external vLLM
815+ adapter: built-in
816+ runtime: /w/e2e-prewarm/data/runtimes/wheel/release-wheel-gfx94x-dcgpu-7-15-0
817+ protocol: 0.1.0
818+ " ;
819+
820+ #[ test]
821+ fn the_marked_engine_is_the_one_pre_warmed ( ) {
822+ // Which engine to install comes from the CLI's own host detection, not
823+ // from a hardware map duplicated here.
824+ assert_eq ! ( default_engine_from_inventory( ENGINES_READY ) , Some ( "vllm" ) ) ;
825+ }
826+
827+ #[ test]
828+ fn an_indented_detail_line_is_not_read_as_the_default ( ) {
829+ // Every engine's detail lines are indented under it, and a note may well
830+ // start with a bullet. Only the marker in column 0 names the default.
831+ let inventory = "\
832+ Local model engines
833+ * lemonade default embedded Lemonade server with ROCm llama.cpp backend
834+ adapter: built-in
835+ * not a marker
836+ " ;
837+ assert_eq ! ( default_engine_from_inventory( inventory) , Some ( "lemonade" ) ) ;
838+ }
839+
840+ #[ test]
841+ fn an_inventory_without_a_default_engine_names_none ( ) {
842+ // `ensure_default_engine` turns this into an error rather than guessing an
843+ // engine: installing the wrong one costs a multi-GiB build and still
844+ // leaves the lane with nothing to serve on.
845+ assert_eq ! ( default_engine_from_inventory( "" ) , None ) ;
846+ assert_eq ! (
847+ default_engine_from_inventory( "Local model engines\n lemonade embedded\n " ) ,
848+ None
849+ ) ;
850+ }
851+
852+ #[ test]
853+ fn reusing_the_shared_runtime_still_reaches_the_engine_check ( ) {
854+ // The regression this guards: `Reuse` — the decision EVERY warm runner
855+ // takes, run after run — used to end `run` with a `return` inside its own
856+ // match arm, before anything looked at the engine. The early return is now
857+ // this predicate, read only AFTER `ensure_default_engine`, so reuse cannot
858+ // skip the engine. Reuse must still install and update NOTHING, which is
859+ // what keeps it cheap enough to re-check the engine on every run.
860+ assert ! ( !runtime_changed( & Decision :: Reuse {
861+ reason: "up to date" . to_owned( )
862+ } ) ) ;
863+ assert ! ( runtime_changed( & Decision :: Install ) ) ;
864+ assert ! ( runtime_changed( & Decision :: Update {
865+ runtime_key: "release-wheel-gfx94x-dcgpu-7-13-0" . to_owned( )
866+ } ) ) ;
867+ }
868+
734869 #[ test]
735870 fn no_managed_runtime_installs ( ) {
736871 assert_eq ! ( decide( EMPTY , "release" ) , Decision :: Install ) ;
0 commit comments