Skip to content

Launch models with their own engine image and flags (Nemotron 3.5 Lightning, Qwen3.8-27B) - #62

Open
webdevtodayjason wants to merge 6 commits into
mainfrom
fable/0.5.4-native-engines
Open

Launch models with their own engine image and flags (Nemotron 3.5 Lightning, Qwen3.8-27B)#62
webdevtodayjason wants to merge 6 commits into
mainfrom
fable/0.5.4-native-engines

Conversation

@webdevtodayjason

Copy link
Copy Markdown
Contributor

Problem

Two models we validated on the GB10 fleet could not be launched by AInode at all, only by hand-rolled docker run commands. That meant the two fastest and most capable models on the cluster were invisible to the dashboard, missing from /v1/models, and unreachable through the master node's federated endpoint. If a node rebooted they were simply gone.

Three things blocked them:

  • _build_vllm_serve_args emitted a fixed flag set with no passthrough, so recipes needing speculative decoding, MoE/mamba backends, or reasoning and tool-call parsers could not be expressed.
  • The engine image was fleet-global via $NVIDIA_VLLM_IMAGE. Both models need vLLM 0.27.1 while the fleet default is a 0.17 build.
  • --enforce-eager and the NVFP4 MARLIN env were applied unconditionally. They are workarounds for a FlashInfer bug in the 0.17 build, and on 0.27.1 they only disable CUDA graphs and cost throughput.

Two related failure modes made every launch problem undebuggable: engines ran with docker run --rm -d, so one that died during startup deleted itself and left no logs and no corpse (about 18 self-erased containers were found on spark-4), and start_solo() returned success as soon as the docker CLI forked, so a crashed engine still registered as a live instance.

Fix

NodeConfig gains extra_vllm_args and engine_image, threaded through _OVERRIDE_KEYS so they persist and survive restart-replay like every other per-load override. A flag supplied by the caller suppresses the matching built-in rather than appearing twice, since vLLM errors on duplicates. The legacy GB10 workarounds now apply only when the instance runs the pinned default image.

Curated catalog entries for both models carry their proven recipe (image, flags, recommended memory fraction), applied as defaults so a bare {"model": "..."} load, which is what the dashboard sends, launches correctly.

Dropped --rm, and start_solo() now confirms the container reached Running and surfaces the engine's last output when it did not.

Two bugs turned up while verifying on hardware and are fixed here too:

  • vllm/vllm-openai bakes ENTRYPOINT ["vllm","serve"] while the default image uses NVIDIA's passthrough shim, so our args produced vllm serve vllm serve <model>. The launch path now inspects the image entrypoint and emits only the missing tokens. It deliberately does not override the entrypoint, which would bypass CUDA setup, and an unreadable image falls back to the legacy prefix.
  • The automatic fp8 to auto KV downgrade for multimodal models only fires for models on local disk because it reads config.json. Qwen3.8 serves from the HF cache, so it was getting fp8 KV, which corrupts vision output on GB10. The recipe states auto explicitly.

Tests

696 pass, ruff clean. 26 new tests cover recipe passthrough, flag dedup in both --flag value and --flag=value forms, image gating of the legacy workarounds, entrypoint normalization, and launch confirmation. Two existing nvidia-backend tests were updated to the stronger launch contract, since "the docker CLI forked" no longer counts as a successful start.

Verified end to end on spark-3. A bare POST /api/models/load {"model":"unsloth/Qwen3.8-27B-NVFP4"} produced this command:

IMAGE: vllm/vllm-openai:v0.27.1
ARGS : serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000
       --gpu-memory-utilization 0.6 --enable-prefix-caching --kv-cache-dtype auto
       --reasoning-parser qwen3 --tool-call-parser qwen3_coder --enable-auto-tool-choice
       --speculative_config {"method":"qwen3_5_mtp","num_speculative_tokens":2}

It served chat, emitted correct tool calls (proving the parser applied), answered a vision prompt correctly, and ran at 18.1 tok/s with MTP speculative decoding. The model now appears on the master node's /v1/models and a request sent there routes to spark-3 and comes back. That was not possible before this change.

Nemotron 3.5 Lightning uses the identical mechanism and its recipe is unit-tested, but it has not yet been launched through the API on hardware. That is tracked in FOLLOWUPS.md along with pre-staging its DSpark drafter.

Default behavior is unchanged for every model that does not carry a recipe. The generated command for the pinned default image is byte for byte what it was before.

webdevtodayjason and others added 5 commits July 7, 2026 08:41
…unch verification

Models whose published recipe needs flags AINode doesn't model (speculative
decoding, MoE/mamba backends, reasoning + tool-call parsers) could not be
launched through the product at all. Nemotron 3.5 Lightning and Qwen3.8-27B were
both validated on the GB10 fleet only as hand-rolled `docker run` containers,
which left them invisible to the dashboard, to /v1/models, and to the federated
router on the master node.

Problem
- `_build_vllm_serve_args` emitted a fixed flag set with no passthrough.
- The engine image was fleet-global ($NVIDIA_VLLM_IMAGE); both models need
  vLLM 0.27.1 while the fleet default is a 0.17 build.
- `--enforce-eager` and the NVFP4 MARLIN env are 0.17-era GB10 workarounds but
  were applied unconditionally; on 0.27.1 they only cost throughput.
- `docker run --rm -d` meant an engine that died during startup erased itself,
  so failures left no logs and no corpse (~18 self-erased containers observed).
- `start_solo()` returned True once the docker CLI forked, so a crashed engine
  still registered as a live instance.

Fix
- NodeConfig gains `extra_vllm_args` and `engine_image`, threaded through
  _OVERRIDE_KEYS so they persist and survive restart-replay like every other
  per-load override. A caller-supplied flag suppresses the same built-in rather
  than duplicating it (vLLM errors on duplicates).
- Legacy GB10 workarounds now apply only to the pinned default image.
- Dropped `--rm`; start_solo() confirms the container reached Running and
  surfaces the engine's last output when it did not.
- Catalog entries for both models carry their proven recipe, applied as defaults
  on load so a bare {"model": ...} launches correctly.

Tests: 21 new (recipe passthrough, dedup, image gating, launch confirmation);
two nvidia-backend tests updated to the stronger launch contract. 691 pass.
…ache auto for the Qwen3.8 VLM recipe

Caught on hardware verifying the load path:

1. vllm/vllm-openai bakes ENTRYPOINT ["vllm","serve"] while the pinned default
   uses NVIDIA's passthrough shim. Emitting our own "vllm serve" produced
   `vllm serve vllm serve <model>` and the engine exited with "unrecognized
   arguments". _serve_argv_prefix() now inspects the image ENTRYPOINT and emits
   only what's missing; an unreadable image falls back to the legacy prefix so a
   docker hiccup can't change how the default image launches. Entrypoint is NOT
   overridden — that would bypass nvidia_entrypoint.sh's CUDA setup.

2. The automatic fp8->auto KV downgrade for multimodal models only fires when the
   model is on local disk (it reads config.json); Qwen3.8 serves from the HF
   cache, so it was getting fp8 KV — which corrupts VLM generation on GB10. The
   recipe now states --kv-cache-dtype auto explicitly (the existing dedup makes
   it suppress the built-in).

The crashed container left a readable corpse, which is the --rm removal from the
previous commit working as intended.
CI went red with 683 findings in files nobody touched (import ordering, dict()
literals, and so on) while the same command was green on main in July. Cause is
the dev dependency `ruff>=0.1.0` being unpinned: newer ruff releases keep widening
the DEFAULT rule set, so CI silently started enforcing rules this repo never opted
into, and every PR opened today fails the same way.

Selecting E4/E7/E9/F explicitly restores the repo's actual intent and makes lint
deterministic regardless of which ruff CI resolves. The whole repo passes clean
under it. Widening the set is worth doing, but as a deliberate cleanup rather than
something a transitive upgrade inflicts on an unrelated PR.
@webdevtodayjason
webdevtodayjason force-pushed the fable/0.5.4-native-engines branch from a88fa34 to 5dcb7dc Compare August 15, 2026 15:56
… path

Two of the three remaining robustness bugs from dogfooding the GB10 fleet.

Eject was memory-only. It dropped the instance from the manager but never
rewrote the manifest that startup replay reads, so an ejected model came back on
the next reboot. On spark-4 an ejected 0.5B reappeared and then blocked a later
load through admission control, with nothing in the UI to explain why. Eject now
persists the shrunken instance set, and clears the node's config.model claim when
the ejected instance was the primary so the master stops advertising a ghost.
Losing the manifest write no longer fails the operator's eject.

The boot path chose the legacy host-venv VLLMEngine whenever the node was not
detected as containerized, regardless of the configured backend. Inside the slim
orchestrator image vLLM is deliberately absent, so that engine dies with
"No module named 'vllm'" while the banner still prints "Engine starting in
background" and the node looks healthy while serving nothing. The dispatch now
checks whether vLLM is importable and falls back to the configured container
backend instead of launching a certain failure.

The third bug (stale fleet-wide advertisement) is NOT fixed here on purpose. The
liveness gating already exists at api/server.py:441 and the fleet menu is truthful
again after a restart, so the symptom points at stale cluster records on a
long-uptime node rather than a missing check. Filed with a concrete repro to run
instead of guessing at a fix.

Tests: 6 new (eject persistence, primary-claim clearing, stacked isolation,
persistence-failure tolerance, boot dispatch ordering). 702 pass, ruff clean.
@webdevtodayjason
webdevtodayjason force-pushed the fable/0.5.4-native-engines branch from 5dcb7dc to 7a6613e Compare August 15, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant