A platform team can now choose the serving stack each Modelplane cluster runs.
With Standard, Modelplane composes the serving stack, as it always has. With
Dynamo, it uses NVIDIA Dynamo's
components instead: Grove and the KAI
Scheduler place a multi-node engine as
a gang, and a ModelExpress server
moves model weights GPU to GPU between replicas.
Both of those save GPU time you're paying for either way. A multi-node engine is a gang, and its leader and workers are useless apart, because each holds a shard of the model's weights. Schedule those pods one at a time and a gang can half-land, holding the GPUs it won while serving nothing, waiting for nodes that may not be free for a while. KAI places the whole gang or none of it.
Loading weights is the other cost. Each replica reads the model from storage before it can serve a token, and several replicas scaling up together queue behind each other on the same volume. ModelExpress makes that one read rather than one per replica: the first replica loads from storage and the rest pull the weights from a peer's GPU across the cluster's fabric.
The ModelDeployment an ML team writes stays the same. The same manifest runs
on either stack. Which stack a cluster runs is a platform decision, made per
cluster, so a fleet can run both at once. Both stacks are in v0.4.
A serving stack owns one cluster
Modelplane operates a fleet. It provisions clusters and node pools, schedules each replica onto hardware that fits, stages weights once per cluster, scales replicas, and fronts the whole fleet with one OpenAI-compatible endpoint. It isn't a serving layer itself.
A serving stack owns what happens inside one cluster. It places a multi-node engine's pods and gets the model's weights into GPU memory. Dynamo does both, with a frontend and router ahead of the engines, and it reaches into territory Modelplane's standard stack doesn't, like keeping those weights resident across an engine crash.
A ModelDeployment describes an engine
The ML team writes one container named engine with its image, command, and
args, and what they write is what runs. An engine is either Standalone, or a
Leader and a Worker whose command spans nodes. A ModelDeployment describes
that engine and says nothing about the stack underneath it.
So a serving stack has to run two pod specs with distinct commands, and give a worker a way to find its leader. Grove does both.
Opting a cluster in
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: eks-h200-us-east
spec:
# Standard (the default) or Dynamo. Immutable.
stack: Dynamo
cluster:
source: EKS
eks:
region: us-east-1
nodePools:
- name: gpu
className: eks-h200-8x
nodeCount: 2On a Dynamo cluster Modelplane installs Grove, the KAI Scheduler, and one
ModelExpress server. On a Standard cluster it installs the
LeaderWorkerSet controller. Everything
else about a cluster, from how it fronts requests to how it stages model
weights, is the same on both.
The choice is immutable, which makes adoption incremental. A platform team stands up a Dynamo cluster next to the ones it already runs and moves deployments over cluster by cluster.
Gang scheduling with Grove and the KAI Scheduler
A Standalone engine is a Deployment on both stacks. A Leader and Worker
gang is a LeaderWorkerSet on Standard, and on Dynamo a Grove PodCliqueSet
with a leader clique and a worker clique, scheduled by KAI.
Here's a 480B model across two nodes, tensor-parallel within each node and
pipeline-parallel across them, that also opts into ModelExpress.
$(MODELPLANE_LEADER_ADDRESS) is the address the leader is reachable at, and it
resolves on both stacks:
apiVersion: modelplane.ai/v1alpha1
kind: ModelDeployment
metadata:
name: qwen3-coder
namespace: ml-team
spec:
replicas: 1
template:
spec:
modelCacheRef:
name: qwen3-coder
engines:
- name: qwen3-coder
members:
- role: Leader
nodeSelector:
devices:
- name: gpu
count: 8
selectors:
- cel: |
device.capacity["gpu.nvidia.com"].memory.compareTo(quantity("120Gi")) >= 0
template:
spec:
containers:
- name: engine
image: vllm/vllm-openai:v0.23.0
command:
- /bin/sh
- -c
- >-
pip install --index-url https://pypi.nvidia.com modelexpress &&
exec vllm serve Qwen/Qwen3-Coder-480B-A35B-Instruct
--served-model-name=qwen3-coder
--load-format modelexpress
--tensor-parallel-size=8
--pipeline-parallel-size=2
--distributed-executor-backend=mp
--nnodes=2 --node-rank=0
--master-addr=$(MODELPLANE_LEADER_ADDRESS)
--max-model-len=32768
--port=8000
- role: Worker
worker:
nodes: 1
# nodeSelector is the same as the leader's. Omitted for brevity.
template:
spec:
containers:
- name: engine
image: vllm/vllm-openai:v0.23.0
command:
- /bin/sh
- -c
- >-
pip install --index-url https://pypi.nvidia.com modelexpress &&
exec vllm serve Qwen/Qwen3-Coder-480B-A35B-Instruct
--served-model-name=qwen3-coder
--load-format modelexpress
--tensor-parallel-size=8
--pipeline-parallel-size=2
--distributed-executor-backend=mp
--nnodes=2 --node-rank=1
--master-addr=$(MODELPLANE_LEADER_ADDRESS)
--headless
--max-model-len=32768The modelCacheRef names a ModelCache, which stages a model's weights once
per cluster on shared storage. Both members name the model by its Hugging Face
repo id, and Modelplane points the engine's HF_HUB_CACHE at the mount, so the
engine resolves that repo id against the staged snapshot instead of downloading
it.
Weight transfer with ModelExpress
A Dynamo cluster runs one ModelExpress server. It brokers which replica holds
a model in GPU memory, and never touches the weight bytes itself. The first
replica loads from the cache volume and publishes itself as a source, and later
replicas pull the weights from a peer's GPU over RDMA, across a fast fabric like
EFA on EKS. A replica that finds no peer, or no fabric to reach one over, reads
the cache volume instead, so size and keep the cache for every replica on either
stack.
Modelplane doesn't add a load format of its own, so --load-format modelexpress
above is the ML team's opt-in rather than something the stack injects. That's
what keeps the manifest portable. Run it on a Standard cluster, where nothing
runs a ModelExpress server, and the engine reads the cache volume.
What's next
The end state is the same cluster opt-in composing a full
DynamoGraphDeployment, so a fleet gets Dynamo's frontend and router while the
API an ML team writes stays what it is. NVIDIA has the upstream work in flight.
A component's worker pod spec needs to be able to differ from its leader's, and
the operator needs a switch to leave the user's command alone rather than
generating launch flags, both of which are in
dynamo#12696. The engine
also runs today as a Dynamo runtime image rather than the stock vllm serve an
ML team writes everywhere else, which
dynamo#10835 addresses by
moving the runtime wrapper into a sidecar.
Try it
The getting-started guide covers standing up a fleet, and how it works covers what a serving stack installs. Modelplane is Apache 2.0 and moving fast at github.com/modelplaneai/modelplane, and questions are welcome in Slack.





