NVIDIA Dynamo · KubernetesDocs baseline: Dynamo v1.5.1

Planner

×

Grove

Planner decides how many engines to run. Grove decides what one engine instance is and where it lands.

Interactive: slides 7, 8, 9, 13, 15F full screen← → navigateN notes
The problem

Three problems a generic autoscaler can’t solve for LLM serving

① Two bottlenecks Prefill TTFT whole prompt at once · compute‑bound Decode ITL one token per step · memory‑bound One replica count can’t fit bothNeed: size each role on its SLA ② Requests ≠ load same 10 requests / s ISL 500 5k tokens/s ISL 8,000 80k tokens/s 16× the prefill work Request rate alone mis‑sizes capacityNeed: forecast traffic shape ③ Capacity is multi‑node one TP16 decode instance 2 pods · 16 GPUs · works half an instance = 0 capacity Partial placement wastes GPUsNeed: place whole units only
PlannerGrove

How Dynamo solves them: Planner + Grove

traffic ISL / OSL latency solves ① ② Planner How many? ① a count per role ② req · ISL · OSL TTFT / ITL targets DGD prefill: replicas: 4 decode: replicas: 2 the contract solves ③ Grove What & where? ③ whole instances placed all‑or‑nothing ordered, topology‑aware cluster Planner never calls Grove. The Dynamo operator reconciles the DGD into Grove resources.
PlannerHow it works today

Planner runs one pipeline every tick

repeat every tick 1Observe 2Predict 3Propose 4Constrain 5Execute traffic +engine metrics next interval:req · ISL · OSL replicaproposals minimums ·GPU budget scale_to →connector
Perf model from the first available:self‑benchmark→AI Configurator→profiling data→live FPM regression
Which proposers run depends onoptimization_target(next slide) ·advisory: truerecommends without scaling
PlannerConfig · optimization_target

Two ways to tell Planner when to scale

A · Scale on engine load“Add an engine when the current ones are busy.”KV‑cache demand →150%throughputdefaultscale up at KV > 100% or queue ≥ 1× contextlatencyscale up earlier: KV > 40% or queue ≥ 0.1×loadyou set the up / down thresholdsEvery 5 s · no latency targetsB · Scale to latency targets“Keep TTFT ≤ 500 ms and ITL ≤ 50 ms.”You setoptimization_target: slattft_ms: 500itl_ms: 50Planner addsForecast every 180 ssets a replica floor ahead of demandLive check every 5 s±1 on measured latency (enable_load_scaling)The mode the next slides explain →
Plannersla target with load scaling on · both loops

How Planner makes the autoscaling decision

1 · every 180 s

Forecast a floor

floor = ⌈ forecast req/s
÷ req/s one engine
  serves at the SLA ⌉

Capacity is ready before the demand arrives.

2 · every 5 s

Check live latency

▲ +1 every engine over SLA
▼ −1 load fits on N−1 engines
● hold otherwise

Estimated per engine from forward‑pass metrics.

3 · combine

Never below the floor

desired =
max(live decision, floor)

Short dips don't remove forecast capacity.

4 · guardrails

Then apply

no +1 while replicas start
≥ min_endpoint
GPU budget caps growth

Result is written to the DGD replica count.

① floor (180 s) ②③ replicas ±1 every 5 s, ≥ floor demand Burst: live loop adds +1 steps until the next forecast catches up.
PlannerPredictive loop · optimization_target: sla

How the throughput loop picks a replica count

Engines: prefill TP2 (2 GPUs), decode TP4 (4 GPUs). Curves are illustrative; real ones come from self‑benchmark, AI Configurator or profiling.

PlannerReactive loop · sla mode

How the load loop decides +1, −1 or hold

Shown for prefill/TTFT. Decode uses scheduled + queued KV tokens → ITL (÷ accept length). ±1 per tick; no scale‑up while replicas are starting; never below the throughput floor.

PlannerInteractive · illustrative capacities

The decision rules, playing out over an hour

① Forecast every 180 s sets the floor Target replicas · shaded = ready (~60 s start) ▲ ▼ live ±1 decisions Click the timeline → cards show the math at that moment
PlannerGroveActuation path

One scale decision, end to end

DECIDEDECLARETRANSLATEGROUPPLACESTART Plannerdecode2 → 3 DGDreplicas: 3(DGDSA if on) DynamooperatorDGD → Grove GrovePCSG = 3+1 PodGang KAIall‑or‑nothingor stays Pending Podsworker → leaderthen Ready new engines register → Planner counts ready capacity → next decision
GroveResource mapping

What Planner’s number actually scales

kind: DynamoGraphDeployment spec.services: prefill: replicas: 4 gpu: 2 decode: replicas: 2 multinode: nodeCount: 2 gpu: 8 # per node # Planner writes the replicas lines PodCliqueSetone per DGD PodClique · prefillreplicas 4 pod pod pod pod PodCliqueScalingGroup · decodereplicas 2 instance = 1 PodGang leader worker node Anode B

replicas = complete instances · nodeCount = pods per instance · each instance = one PodGang

LeaderWorkerSetGroveSame service, two views

What does the scheduler treat as “one unit”?

Example service: 2 prefill + 1 decode instances, each instance = a leader pod + a worker pod.

LeaderWorkerSet LWS object · prefill LWS · decode ✗ 3 separate gangs nothing says prefill needs a decode to be useful Groveone PodCliqueSet for the whole service Base PodGang · start together Scaled gang 1 “minimum service” gang + extras minAvailable decides what goes in the base gang
LWSGrove
Leader + workers of one instance placed together✓✓
Prefill + decode placed together (a service that can actually answer)✗✓
Startup order across roleswithin a group✓
Scale one role without touching the other✓✓
LeaderWorkerSetGroveInteractive · step through

Why it matters when GPUs are scarce

LeaderWorkerSet

Grove

GroveOn every scale event

Scale‑out adds a complete instance

Gang scheduling

Whole instance or nothing

Hierarchical gangs

Never prefill without decode

Startup ordering

Roles start in declared order

Topology‑aware

Set via topologyConstraint

Gang termination

Capacity count stays honest

Coherent updates

Prefill + decode roll together

PlannerGroveInteractive · simplified model

Watch a scale decision reach the cluster

Try Burst: the load loop steps +1 before the floor rises. Budget 40 → clamping. Budget 80 + peak 70 + TP16 → the extra decode PodGang stays Pending, never half‑placed. 1 tick = 5 s; throughput loop every 60 s here. Numbers are illustrative.

Sim time
0:00
Traffic
0 req/s
GPUs / budget
–
Est. TTFT
–
Est. ITL
–
SLA met
–

Traffic vs forecastreq/s

observedPlanner forecast

Replicas: target vs readyengines

prefill targetreadydecode targetready

Latency as % of SLA100% = SLA

TTFTITLSLA

Cluster · 8 nodes × 8 GPUs–

prefilldecodestriped = startingoutlined = waiting

Grove resources (from DGD)PodGangs

Event lognewest first

PlannerBeyond one DGD

One Planner per DGD; GlobalPlanner coordinates many

clients Control DGD Frontend GlobalRouter GlobalPlanner --max-total-gpus --managed-namespaces Prefill pool · short ISL LocalRouter Planner own PodCliqueSet Prefill pool · long ISL LocalRouter Planner own PodCliqueSet Decode pool LocalRouter Planner own PodCliqueSet ⇢ scale requestsbudget check, then apply → requestsrouted by ISL + SLA target
Operating notes

Things that bite in practice

Δt

Interval ≥ time‑to‑ready

Otherwise ticks just hold. Multinode gangs are slowest.

μ

Averages hide tails

Bimodal ISL? Split into pools under GlobalPlanner.

1

One autoscaler per service

HPA on Frontend + Planner on workers. Never both on decode.

≥1

No scale‑to‑zero

Zero workers = model leaves /v1/models, no signal to return.

≠

Budget ≠ cluster

Planner caps the ask; KAI decides what fits. Misfits stay Pending.

1.5

Version pairing

KAI ≥ v0.17.0 · Grove ≥ v0.1.0‑alpha.13 when installed externally.

Planner × Grove · NVIDIA Dynamo