AI Dynamo#
Measure serving performance with distributed prefill and decode workers using test_template_name = "AIDynamo".
Choose Kubernetes or Slurm below, then inspect measurements using Read results and generate reports.
Run on Kubernetes#
Prepare kubernetes.toml using Run on Kubernetes, then install the
Dynamo operator and platform services
for your runtime version. The operator must serve nvidia.com/v1alpha1 DynamoGraphDeployment resources.
The supplied example uses:
The public
nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.1.1image andQwen/Qwen3-0.6Bmodel.One prefill worker and one decode worker, each with eight GPUs; set
gpus_per_node = 8in the system config.GenAI-Perf to send requests and collect inference measurements.
Use a dedicated namespace for the experiment. CloudAI names the deployment after default_namespace;
run one Dynamo experiment at a time in that namespace.
For accelerated transfers between workers, configure Network Operator attachments.
uv run cloudai run --system-config kubernetes.toml \
--tests-dir conf/experimental/ai_dynamo/test \
--test-scenario conf/experimental/ai_dynamo/test_scenario/vllm_k8s.toml
View the scenario
name = "vllm_k8s"
[[Tests]]
id = "dynamo.vllm"
test_name = "vLLM"
[Tests.cmd_args]
[Tests.cmd_args.dynamo]
[Tests.cmd_args.dynamo.prefill_worker]
num-nodes = 1
[Tests.cmd_args.dynamo.prefill_worker.args]
tensor-parallel-size = 8
[Tests.cmd_args.dynamo.decode_worker]
num-nodes = 1
[Tests.cmd_args.dynamo.decode_worker.args]
tensor-parallel-size = 8
To change worker sizing, copy the scenario and edit each worker’s num-nodes and tensor-parallel-size.
Keep the tensor parallel size consistent with the GPUs available to that worker. Workers spanning multiple nodes
also need Dynamo’s multinode orchestration components on the cluster.
The Kubernetes path uses cmd_args.genai_perf settings from the vLLM test definition.
Inspect deployment.yaml, genai_perf.log, and the collected GenAI-Perf artifacts in the case output directory.
Run Using Slurm#
Node Configuration for AI Dynamo#
AI Dynamo jobs use three distinct types of nodes:
Frontend node: Hosts the coordination services (etcd, nats), the frontend server, the request generator (aiperf by default, configurable via
workloadsin the test TOML), and the first decode workerPrefill node(s): Handle the prefill stage of inference
Decode node(s): Handle the decode stage of inference (optional, depending on model and setup)
By default, when num_nodes is omitted, CloudAI allocates separate nodes for prefill and decode workers:
num_prefill_nodes + num_decode_nodes
Set top-level num_nodes explicitly to control the Slurm allocation. A value lower than
num_prefill_nodes + num_decode_nodes enables shared-node disaggregated inference, where prefill and decode roles
run on the same allocated node(s) with separate GPU slices.
All node role assignments and orchestration are automatically managed by CloudAI.
Multinode Backend Workers on Slurm#
prefill_worker.num-nodes and decode_worker.num-nodes specify the total nodes assigned to each role.
Set nodes-per-worker to group those nodes into logical backend workers. For example, the following configuration
creates one prefill worker spanning two nodes and one decode worker spanning two other nodes:
[cmd_args.dynamo.prefill_worker]
num-nodes = 2
nodes-per-worker = 2
[cmd_args.dynamo.prefill_worker.args]
tensor-parallel-size = 8
pipeline-parallel-size = 1
[cmd_args.dynamo.decode_worker]
num-nodes = 2
nodes-per-worker = 2
[cmd_args.dynamo.decode_worker.args]
tensor-parallel-size = 8
pipeline-parallel-size = 1
When nodes-per-worker is omitted, it defaults to 1 and preserves the previous one-worker-per-node behavior.
For vLLM TP/PP groups, CloudAI launches rank 0 as the Dynamo-facing process and the remaining ranks with
--headlessusing Dynamo’s multiprocessing executor.When
data-parallel-sizeis greater than one, CloudAI instead launches a full Dynamo vLLM process on every node and derivesdata-parallel-size-local,data-parallel-start-rank, the coordinator address, and the RPC port from the worker topology.SGLang receives
--dist-init-addr,--nnodes, and--node-rankon every participating node; its configureddata-parallel-sizeis forwarded asdp-size.The launcher derives prefill and decode disaggregation modes from the configured roles, so worker commands do not need role flags.
For TP/PP-spanning workers, tensor-parallel-size * pipeline-parallel-size must be divisible by nodes-per-worker.
For vLLM multinode data parallelism,
data-parallel-sizemust instead be divisible bynodes-per-worker, and each DP rank’s TP/PP group must fit within one node.SGLang multinode data parallelism requires
--enable-dp-attention.The resulting per-node GPU footprint must fit on each node.
Ray-backed Dynamo vLLM groups are not currently supported.
The Slurm launcher explicitly selects etcd discovery and the NATS event plane.
No separate WideEP mode is required.
Multinode data-parallel orchestration is inferred from
nodes-per-worker > 1anddata-parallel-size > 1.Backend expert-parallel parameters such as vLLM’s
--enable-expert-parallelor SGLang’sep-sizeand--enable-dp-attentionremain explicit backend configuration.CloudAI uses the allocated Slurm node names for multinode rendezvous.
The cluster must make those names consistently resolvable and mutually reachable from every allocated node and workload container.
Multi-homed systems should also configure backend network selection, such as
GLOO_SOCKET_IFNAMEandNCCL_SOCKET_IFNAME, at the system level.Explicit backend rendezvous addresses take precedence over addresses derived from the Slurm node names.
Clusters that cannot provide this hostname contract must supply backend-native per-node address overrides, such as
SGLANG_HOST_IPorVLLM_NIXL_SIDE_CHANNEL_HOST, together with explicit worker rendezvous arguments.
Dedicated examples are available in test_scenario/vllm_multinode_worker_slurm.toml and
test_scenario/sglang_multinode_worker_slurm.toml. The existing vllm_slurm.toml and sglang_slurm.toml
scenarios retain their previous configurations. The vLLM example inherits tensor-parallel-size = 8 from
test/vllm.toml; the SGLang example sets it explicitly because test/sglang.toml defaults to 1.
WideEP examples are provided in test_scenario/vllm_wideep_slurm.toml and
test_scenario/sglang_wideep_slurm.toml.
Launch and Monitor the Job#
To run the job:
uv run cloudai run --system-config <slurm system toml> \
--tests-dir conf/experimental/ai_dynamo/test \
--test-scenario conf/experimental/ai_dynamo/test_scenario/vllm_slurm.toml
The job progress monitoring can be done using either of the following options:
watch squeue --me
watch tail -n 4 ./results/<scenario name>/*.txt
The frontend node will initially wait to allow weight loading on all nodes. Once ready, it will launch the configured benchmark tool (aiperf by default), which begins generating requests to the frontend server. All servers cooperate to complete inference, and the output will appear in stdout.txt.
Choosing a Benchmark Tool#
The benchmark tool is controlled by the workloads field in the test TOML. Set aiperf.sh to use AIPerf:
[cmd_args]
workloads = "aiperf.sh" # uses aiperf, writes aiperf_report.csv
To use genai-perf, set:
[cmd_args]
workloads = "genai_perf.sh" # uses genai-perf, writes genai_perf_report.csv
[cmd_args.genai_perf]
cmd = "genai-perf profile"
extra-args = "--streaming --verbose -- -v --async"
[cmd_args.genai_perf.args]
endpoint-type = "chat"
output-tokens-mean = 500
request-count = 50
AIPerf Multi-Phase Runs#
cmd_args.aiperf is the base AIPerf config. cmd_args.aiperf_phases can run several AIPerf rounds against the
same live Dynamo stack. By default, CloudAI does not restart prefill, decode, or router processes between phases:
dse_excluded_args = ["cmd_args.aiperf_phases"]
[cmd_args.aiperf]
health-check-between-phases = true
between-phase-cmd = "true" # default no-op
[cmd_args.aiperf.args]
request-count = 50
server-metrics = "auto"
[[cmd_args.aiperf_phases]]
name = "round_1"
[cmd_args.aiperf_phases.args]
concurrency = 2
[[cmd_args.aiperf_phases]]
name = "round_2"
[cmd_args.aiperf_phases.args]
concurrency = 4
Single-phase runs keep the old artifact layout: aiperf_artifacts/, aiperf.log, and aiperf_report.csv.
Multi-phase runs write per-phase artifacts/logs/reports and copy the last phase report to aiperf_report.csv for
existing report generation.
between-phase-cmd is a bash command run after each non-final phase. The default is a no-op. Set it explicitly for
backend-specific cache cleanup, for example /cloudai_run_results/routerctl.sh restart if a test needs to restart the
Dynamo router between phases. health-check-between-phases probes the frontend after the command.
Comparison Reports#
CloudAI provides the scenario-level ai_dynamo_comparison report. It compares AI Dynamo runs using the standard
LLM serving tables and charts for TTFT, TPOT, successful prompts, output-token throughput, TPS per user, and TPS per
GPU. When aiperf_accuracy is configured and its result is available, the report also compares model accuracy.
Both AIPerf and GenAI-Perf CSV output are supported.
The scenario-level Pareto report uses each configured AIPerf phase as a separate point, with that phase’s concurrency and benchmark settings. It also supports GenAI-Perf results.
AIPerf args are rendered as normal CLI flags. Multi-value AIPerf options should be passed with AIPerf CLI syntax, such
as server-metrics-formats = "csv,json,jsonl" or gpu-telemetry = "node1:9401,node2:9401". server-metrics =
"auto" expands to the frontend metrics endpoint, Dynamo worker metrics endpoints, and any CloudAI-started DCGM
exporters.
Propagating LMCache Configuration#
AIDynamo can pass an LMCache YAML config to the worker processes by setting LMCACHE_CONFIG_FILE inside the
container. This only propagates the LMCache configuration; the vLLM/SGLang runtime still needs to be launched with the
appropriate LMCache or KV-transfer connector for that image/version.
The preferred form is structured TOML under [cmd_args.lmcache]. CloudAI converts that object to YAML in the
run output directory, mounts that directory as /cloudai_run_results, and exports the generated file path as
LMCACHE_CONFIG_FILE:
[cmd_args]
[cmd_args.lmcache]
chunk_size = 256
local_cpu = true
controller_pull_url = "{frontend_node}:8300"
controller_reply_url = "{frontend_node}:8400"
lmcache_worker_ports = [8788, 8789, 8790, 8791]
max_local_cpu_size = 6.0
nixl_buffer_size = 2079377920
nixl_buffer_device = "cpu"
[cmd_args.lmcache.extra_config]
enable_nixl_storage = false
nixl_backend = "POSIX"
nixl_path = "{storage_cache_dir}"
nixl_pool_size = 2048
For an example that uses test-in-scenario mode, see
conf/experimental/ai_dynamo/test_scenario/vllm_lmcache.toml. Because the test is fully defined inside the scenario,
--tests-dir is not required when running that example:
uv run cloudai run --system-config <slurm system toml> \
--test-scenario conf/experimental/ai_dynamo/test_scenario/vllm_lmcache.toml
The example sets dse_excluded_args = ["cmd_args.lmcache.lmcache_worker_ports"] because
lmcache_worker_ports is a list-valued LMCache setting, not a DSE sweep dimension. Other list-valued LMCache fields
can still be swept unless their cmd_args. path is also excluded.
Alternatively, mount your own LMCache YAML file with extra_container_mounts and set LMCACHE_CONFIG_FILE through
extra_env_vars:
extra_container_mounts = ["/host/lmcache:/lmcache"]
extra_env_vars = { LMCACHE_CONFIG_FILE = "/lmcache/config.yaml" }
For multi-node LMCache storage tests, any path referenced by the LMCache YAML, such as nixl_path for POSIX-backed
storage, must be visible and writable from every node that is expected to share cached data. A node-local path such as
/tmp is suitable only for single-node smoke tests or configuration propagation checks.
LMCache YAML values can use runtime placeholders. CloudAI renders them inside the Slurm job before launching workers:
{frontend_node}, {frontend_ip}, {results_dir}, and {storage_cache_dir}. Unknown placeholders fail the
run before worker processes start.
Propagating HiCache Configuration#
Define a HiCache storage-backend configuration under [cmd_args.hicache]. CloudAI serializes the object as
hicache-config.toml in the run output directory, exposes its container path through HICACHE_CONFIG_FILE, and
passes the resolved path to both workers as --hicache-storage-backend-extra-config:
[cmd_args]
[cmd_args.hicache.plugin.posix]
active = true
use_uring = "false"
[cmd_args.dynamo.prefill_worker]
extra-args = "--enable-hierarchical-cache"
[cmd_args.dynamo.prefill_worker.args]
hicache-size = 8
hicache-ratio = 0
hicache-write-policy = "write_through"
hicache-mem-layout = "page_first"
hicache-storage-prefetch-policy = "wait_complete"
hicache-storage-backend = "nixl"
[cmd_args.dynamo.decode_worker]
extra-args = "--enable-hierarchical-cache"
[cmd_args.dynamo.decode_worker.args]
hicache-size = 8
hicache-ratio = 0
hicache-write-policy = "write_through"
hicache-mem-layout = "page_first"
hicache-storage-prefetch-policy = "wait_complete"
hicache-storage-backend = "nixl"
The remaining HiCache engine options stay in the worker args sections so they can be configured or swept through
DSE normally.
The runtime replacement mechanism below supports per-node HiCache values. For example, change the POSIX setting above
to use_uring = "${use_uring}" and have the startup manifest replace ${use_uring} with false or true.
The worker receives the resulting per-node TOML path automatically.
For a self-contained single-node smoke test using NIXL’s POSIX plugin, run the following on a node with at least two GPUs (one each for the prefill and decode workers):
uv run cloudai run --system-config <slurm system toml> \
--test-scenario conf/experimental/ai_dynamo/test_scenario/sglang_hicache.toml
Per-node Startup Commands and Runtime Replacements#
On Slurm, startup_cmd runs once per allocated node before the workload. It runs on the host by default. Set
startup_cmd_docker_image to run it in a container using the workload’s mounts:
[cmd_args]
startup_cmd = "/mnt/discovery/discover-device.sh"
startup_cmd_docker_image = "<image-containing-your-startup-tools>"
CloudAI exports CLOUDAI_RUNTIME_FILE to the command. It points to runtime/<node>.json in the test output
directory, or /cloudai_run_results/runtime/<node>.json inside a startup container. Write replacements there:
{
"LMCACHE_CONFIG_FILE": {
"${device}": "/dev/ng4n1",
"${namespace_id}": "1"
},
"HICACHE_CONFIG_FILE": {
"${use_uring}": "false"
}
}
Each top-level key names an environment variable containing a config path. AIDynamo creates a per-node copy, applies literal replacements, then updates the variable to point to that copy.
For lmcache-config.yaml on node-01, the result is lmcache-config.node-01.yaml. The shared source remains
unchanged. LMCache placeholders are rendered afterward in the per-node copy. Missing manifest means no replacements.
Startup runs once per test case in normal and --single-sbatch modes.
If the selected LMCache mode needs a controller, CloudAI can start one on the frontend node:
[cmd_args.lmcache_controller]
cmd = "lmcache_controller --host 0.0.0.0 --port 9000 --monitor-ports {\"pull\":8300,\"reply\":8400}"
This only launches the process. For disaggregated or multi-node runs, the LMCache YAML still needs controller addresses
that resolve to the frontend node from every worker. With the default controller monitor ports, use
controller_pull_url = "{frontend_node}:8300" and controller_reply_url = "{frontend_node}:8400". The
lmcache_worker_ports list must match the number of worker ranks.
Semantic Degradation With AIPerf Accuracy#
AIDynamo uses AIPerf accuracy mode as its semantic degradation signal. Enable it with
[cmd_args.aiperf_accuracy]. This runs after the configured performance workload, so it can be used with either
aiperf.sh or genai_perf.sh:
[cmd_args]
workloads = "aiperf.sh"
[cmd_args.aiperf]
[cmd_args.aiperf.args]
request-count = 50
synthetic-input-tokens-mean = 300
output-tokens-mean = 500
concurrency = 2
[cmd_args.aiperf_accuracy]
entrypoint = "aiperf profile"
setup-cmd = "python -m pip install --break-system-packages --upgrade aiperf==0.8.0"
cli = '''
--model {model}
--url {url}
--endpoint-type chat
--streaming
--artifact-dir {artifact_dir}
--no-server-metrics
--accuracy-benchmark mmlu
--accuracy-n-shots 5
--accuracy-tasks abstract_algebra
--concurrency 10
--extra-inputs '{"temperature":0,"chat_template_kwargs":{"enable_thinking":false}}'
--num-requests 100
'''
When cmd_args.aiperf_accuracy is configured, CloudAI expects AIPerf to produce accuracy_results.csv and exposes
the accuracy metric from its OVERALL row. The metric is reported as a 0.0-1.0 fraction. Keep synthetic prompt
and token-length flags out of this mode; the benchmark dataset should come from AIPerf’s accuracy benchmark.
The entrypoint and cli fields form the accuracy command.
CloudAI expands
{model},{url},{endpoint},{result_dir}, and{artifact_dir}inclibefore launching it.The
setup-cmdfield is optional.It is useful for Dynamo images that include an older system
aiperfbuild without the accuracy benchmark plugins.The example upgrades the image-level
aiperfbefore launchingaiperf profile.MMLU is loaded from
lighteval/mmlu, so either allow Hugging Face dataset access or pre-cache that dataset before running withHF_HUB_OFFLINE/HF_DATASETS_OFFLINEenabled.For Qwen3 models, the example disables thinking mode so short MMLU answers can be parsed as choices.
Custom Accuracy Scripts#
cmd_args.aiperf_accuracy can also launch a custom mounted script instead of AIPerf. Mount the script or its parent
directory with extra_container_mounts and set entrypoint to the in-container command:
extra_container_mounts = ["/host/custom_accuracy:/custom_accuracy"]
[cmd_args.aiperf_accuracy]
entrypoint = "python /custom_accuracy/dummy_accuracy.py"
cli = "--model {model} --url {url} --endpoint {endpoint} --artifact-dir {artifact_dir} --prompt ping"
CloudAI expands placeholders in cli and runs entrypoint with that CLI string. The custom command must write
accuracy_results.csv inside {artifact_dir} with an OVERALL row. CloudAI copies that file to the run output
directory and exposes the same accuracy metric as AIPerf accuracy mode.
Review Benchmark Results#
After job completion, CloudAI places output logs and result files in the designated results directory. The result file name depends on the configured workloads field:
aiperf.sh→aiperf_report.csvgenai_perf.sh→genai_perf_report.csvcmd_args.aiperf_accuracy→accuracy_results.csv
If AIPerf accuracy mode is enabled, CloudAI copies aiperf_accuracy_artifacts/accuracy_results.csv to
accuracy_results.csv in the run output directory and marks the run failed if that file is not produced.
Navigate to ./results/<scenario>/<test-id>/0/ and open the CSV to examine performance metrics.
Supported Backends#
The following backends are available via the conf/experimental/ai_dynamo/test/ directory:
vLLM (
vllm.toml) — use withtest_scenario/vllm_slurm.tomlvLLM multinode worker — use
test_scenario/vllm_multinode_worker_slurm.tomlvLLM WideEP — use
test_scenario/vllm_wideep_slurm.tomlvLLM with LMCache config propagation — use self-contained scenario
test_scenario/vllm_lmcache.tomlsglang (
sglang.toml) — use withtest_scenario/sglang_slurm.tomlSGLang multinode worker — use
test_scenario/sglang_multinode_worker_slurm.tomlSGLang WideEP — use
test_scenario/sglang_wideep_slurm.toml
Both backends use aiperf as the default benchmark tool and support disaggregated prefill/decode.
API Documentation#
Command Arguments#
- class cloudai.workloads.ai_dynamo.ai_dynamo.AIDynamoCmdArgs(
- *,
- dynamo_version: str = 'f7e468c7e8ff0d1426db987564e60572167e8464',
- docker_image_url: str,
- startup_cmd: str | None = None,
- startup_cmd_docker_image: str | None = None,
- storage_cache_dir: str | list[str] | None = '/tmp',
- dynamo: ~cloudai.workloads.ai_dynamo.ai_dynamo.AIDynamoArgs,
- hicache: dict | None = None,
- lmcache: dict | None = None,
- lmcache_controller: ~cloudai.workloads.ai_dynamo.ai_dynamo.LMCacheController | None = None,
- genai_perf: ~cloudai.workloads.ai_dynamo.ai_dynamo.GenAIPerf = <factory>,
- aiperf: ~cloudai.workloads.ai_dynamo.ai_dynamo.AIPerf = <factory>,
- aiperf_phases: list[~cloudai.workloads.ai_dynamo.ai_dynamo.AIPerfPhase] | None = None,
- aiperf_accuracy: ~cloudai.workloads.ai_dynamo.ai_dynamo.AIPerfAccuracy | None = None,
- workloads: str = 'genai_perf.sh',
Bases:
CmdArgsArguments for AI Dynamo.
Test Definition#
- class cloudai.workloads.ai_dynamo.ai_dynamo.AIDynamoTestDefinition(*, name: str, description: str, test_template_name: str, cmd_args: ~cloudai.workloads.ai_dynamo.ai_dynamo.AIDynamoCmdArgs, dse_excluded_args: list[str] = <factory>, extra_env_vars: dict[str, str | ~typing.List[str]] = {}, extra_cmd_args: dict[str, str] = {}, extra_container_mounts: list[str] = [], git_repos: list[~cloudai._core.installables.git_repo.GitRepo] = [], nsys: ~cloudai.models.workload.NsysConfiguration | None = None, predictor: ~cloudai.models.workload.PredictorConfig | None = None, training_report: ~cloudai.models.workload.TrainingReportConfig | None = None, agent: str = 'grid_search', agent_steps: int = 1, agent_metrics: list[str] = ['default'], agent_reward_function: str = 'inverse', agent_config: dict[str, ~typing.Any] | None = None, env_params: dict[str, ~cloudai.configurator.env_params.EnvParamSpec] = <factory>, script: ~cloudai._core.installables.file.File = File(src=PosixPath('/home/runner/work/cloudai/cloudai/src/cloudai/workloads/ai_dynamo/ai_dynamo.sh')), constraints: ~cloudai.workloads.ai_dynamo.ai_dynamo.Constraints = Constraints(prefill_tp_le_decode_tp=True, tp_times_pp_le_gpus_per_node=True), success_marker: str = 'success-marker.txt', failure_marker: str = 'failure-marker.txt')[source]#
Bases:
TestDefinition,InferenceReportProviderTest definition for AI Dynamo.
- classmethod parse_aiperf_json(
- path: Path,
- max_concurrency: int = 1,
Read one phase’s native export; absent statistics remain unavailable.
- classmethod parse_benchmark_csv(
- csv_file: Path,
- max_concurrency: int = 1,
Parse one benchmark or phase CSV without substituting another phase’s output.
- validator restore_script_paths » all fields[source]#
Unwrap script paths in saved TOMLs until the shared serialization fix (#5006455).
- validator ignore_legacy_repository » all fields[source]#
Allow historical dumps containing the removed repository field.
- metric_observations(
- system: System,
- tr: TestRun,
Return canonical metric observations produced by this test run, if supported.
- serving_gpu_count(
- roles: tuple[InferenceRoleConfig | None, ...],
Count configured engine devices, including replicas; attention DP shares TP GPUs.
- inference_reports(
- system: System,
- tr: TestRun,
Return one report per measurement, or an empty list when no results are available.
- property repo: GitRepo#
Return the AI Dynamo repository selected by
cmd_args.dynamo_version.
- property installables: list[Installable]#
Get all installables for this test definition.
- property is_domain_randomization_enabled: bool#
at least one
env_paramsannotation.- Type:
Whether the config declares domain randomization
- is_dse_excluded_arg(path: str) bool#
Return whether a dot-separated cmd_args path should be ignored by DSE.
- is_env_sampled(cmd_args_path: str) bool#
Whether a cmd_args field is env-sampled (env draws it per trial, not the agent).
- validator validate_env_params » all fields#
Validate env_params annotations against cmd_args.
env_paramsis an annotation: each key names acmd_argsfield whose value is the candidate set (the single source of truth), and the entry carries only how to sample. So each key must name a realcmd_argsfield whose value is a candidate list; a scalar is already fixed, so annotating it is a meaningless label and is rejected here. Whenweightsare declared, the list needs >= 2 values and the weights must align 1:1 with it. Sampling, persistence, the per-trial cmd_args overlay, and the cache key all live inCloudAIGymEnv; keeping this shape check in core lets the overlay stay agent- and workload-agnostic rather than re-implemented per workload.