Skip to main content

Multi-Node vLLM Deployment

Use multi-node deployment when a model does not fit one server's GPU memory, or the concurrency target needs tensor and pipeline parallelism spread across several hosts. Metrum Insights forms a Kubernetes cluster across the servers you group together, then deploys the model through llm-d (Gateway API, the Gateway API Inference Extension, and a LeaderWorkerSet) instead of launching a single-process model server on one host.

This is a deployment topology for vLLM specifically, not a separate benchmark tool: it builds on the workload and scenario created in metrumbench-llm Workload And Scenario (same tool, same create_workload/create_scenario shapes for anything not covered below) with framework_code: "vllm". SGLang and TensorRT-LLM do not have a multi-node path today.

Set these environment variables for the examples, alongside the ones from metrumbench-llm Workload And Scenario:

export METRUM_ORG_ID="<org-uuid>"

Prerequisites

  • Every server you plan to group must already be onboarded (see Hardware Onboarding) with p_ip_address set. K3s cluster formation joins workers to the head over that address; a server onboarded without one cannot join a multi-node cluster.
  • Every server in a group must have the same GPU vendor. The cluster's GPU device plugin is selected once, from the head server's detected vendor, and applies to every member; a mixed-vendor group leaves the other members' GPUs unusable to the deployment.
  • Verified live on NVIDIA GPUs only. The bootstrap installs an AMD GPU device plugin and configures RCCL cross-node environment variables as well, but that path has not been confirmed end-to-end the way NVIDIA has. Treat AMD multi-node as unverified until it has been.
  • A cluster group needs at least one server, but multi-node deployment is only useful with two or more.

Group Hardware Into A Cluster

Creates a reusable, org-level group of onboarded servers. A workload later references this group by id; the group itself is not tied to any one project or workload.

curl -fsS -X POST "$METRUM_API_URL/rpc/create_cluster_group" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"p_account_id\":\"$METRUM_ACCOUNT_ID\",
\"p_org_id\":\"$METRUM_ORG_ID\",
\"p_group_name\":\"h200-pair\",
\"p_server_ids\":[101,102]
}" | jq

Parameters:

NameRequiredDescription
p_account_idYesAccount UUID; must match the authenticated caller.
p_org_idYesOrganization UUID that owns the group.
p_group_nameYesDisplay name. A URL-safe group code is derived from it.
p_server_idsYesServer numeric ids (server_config_instances.server_id), from Register Server or List Servers in Hardware Onboarding. Position 0 becomes the cluster's head node.

The response's group_id is the numeric id used everywhere below.

List Cluster Groups

Inspect existing groups, their members, and any workload already using them.

curl -fsS -X POST "$METRUM_API_URL/rpc/list_cluster_groups" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"p_account_id\":\"$METRUM_ACCOUNT_ID\",\"p_org_id\":\"$METRUM_ORG_ID\"}" | jq

curl -fsS -X POST "$METRUM_API_URL/rpc/list_cluster_group_members" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"p_account_id\":\"$METRUM_ACCOUNT_ID\",\"p_group_id\":1}" | jq

Parameters:

RPCParameterRequiredDescription
list_cluster_groupsp_account_idYesAccount UUID.
list_cluster_groupsp_org_idYesOrganization UUID.
list_cluster_group_membersp_account_idYesAccount UUID.
list_cluster_group_membersp_group_idYesCluster group numeric id.

Choosing Tensor and Pipeline Parallelism

Set tensor_parallel_size to the number of GPUs on one machine, and pipeline_parallel_size to the number of machines in the cluster group. vLLM launches every group member as one rank (--nnodes equal to the group's member count, one pod per member), so tensor parallelism stays inside a single node's fast NVLink/PCIe interconnect, and pipeline parallelism is what crosses the slower network between nodes. Do not reverse them: splitting tensor-parallel ranks across nodes puts the all-reduce traffic vLLM runs on every forward pass onto the network link instead of NVLink/PCIe, which is far slower and typically dominates latency.

For a 2-node group where each node has 8 GPUs, set tensor_parallel_size: 8 and pipeline_parallel_size: 2 (16 GPUs total). Both are engine args on the engine_args_set_code referenced below; see Command Templates for the vLLM engine args reference.

No seeded preset carries a pipeline_parallel_size above 1 today (the catalog's TP8 presets, vllm-default included, target a single node), so a genuine multi-node deployment needs its own engine args set with both values explicit. Creating one requires a metrum_admin or service-level caller, not a regular org member's own JWT:

curl -fsS -X POST "$METRUM_API_URL/rpc/create_framework_engine_args_set" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"p_framework_code":"vllm",
"p_version":"0.20.0",
"p_set_code":"vllm-deepseek-r1-tp8-pp2-multinode",
"p_set_name":"DeepSeek-R1 TP8/PP2 multi-node",
"p_args_json":"{\"tensor_parallel_size\":\"8\",\"pipeline_parallel_size\":\"2\"}"
}' | jq

Parameters:

NameRequiredDescription
p_framework_code / p_versionYesMust match an onboarded framework version, e.g. vllm / 0.20.0.
p_set_codeYesStable code referenced as engine_args_set_code below.
p_set_nameNoDisplay name.
p_args_jsonYesJSON object of arg_key: arg_value pairs, validated against that framework version's registered engine arg keys. Values are strings even for numeric/flag args.

An existing p_set_code for the same framework version is replaced, not duplicated, so re-running this call to adjust tensor_parallel_size or pipeline_parallel_size is safe.

Create A Multi-Node Project And Workload

A multi-node workload is created through create_project_with_workloads, not the plain Create Workload call in metrumbench-llm Workload And Scenario: only this RPC stamps the workload's deployment mode and attaches its cluster group selection in the same transaction. workloads[].node_group_selections is what makes a workload multi-node; a workload created here without it behaves like any other single-node metrumbench-llm workload.

curl -fsS -X POST "$METRUM_API_URL/rpc/create_project_with_workloads" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"p_org_id\":\"$METRUM_ORG_ID\",
\"p_payload\":{
\"owner_account_id\":\"$METRUM_ACCOUNT_ID\",
\"project_name\":\"deepseek-r1-h200pair-multinode\",
\"visibility_code\":\"private\",
\"workloads\":[{
\"type\":\"benchmark\",
\"workload_code\":\"deepseek-r1-vllm-multinode\",
\"workload_name\":\"deepseek-r1-vllm-multinode\",
\"tool_code\":\"metrumbench-llm\",
\"model_codes\":[\"deepseek-ai/DeepSeek-R1\"],
\"framework_code\":\"vllm\",
\"version\":\"0.20.0\",
\"engine_args_set_code\":\"vllm-deepseek-r1-tp8-pp2-multinode\",
\"node_group_selections\":[{
\"cluster_group_id\":1,
\"selection_name\":\"h200-pair\"
}],
\"scenarios\":[{
\"scenario_code\":\"isl128-osl1024-c256\",
\"scenario_name\":\"isl128-osl1024-c256\",
\"concurrency\":256,
\"input_sequence_length\":128,
\"output_sequence_length\":1024,
\"num_requests\":1024
}]
}]
}
}" | jq

Parameters:

NameRequiredDescription
p_org_idYesOrganization UUID. Gates access via rpc_permission_map.
p_payload.owner_account_idYesAccount UUID that owns the project.
p_payload.project_nameYesNew project name.
p_payload.workloads[].tool_codeNoDefaults to metrumbench-llm.
p_payload.workloads[].model_codesYesArray of model catalog codes (plural, unlike Create Workload's single p_model_code).
p_payload.workloads[].framework_code / versionYesServing framework and version. Use vllm; SGLang and TensorRT-LLM have no multi-node path.
p_payload.workloads[].engine_args_set_codeNoEngine argument preset. Tensor and pipeline parallelism (tensor_parallel_size, pipeline_parallel_size) for the deployment are read from here; use the set created in Choosing Tensor and Pipeline Parallelism, not a single-node preset like vllm-default.
p_payload.workloads[].node_group_selections[].cluster_group_idYesgroup_id from Group Hardware Into A Cluster. Exactly one selection is supported per workload today.
p_payload.workloads[].node_group_selections[].selection_nameYesDisplay name for this workload's use of the group.
p_payload.workloads[].scenarios[]NoSame fields as Create Scenario in metrumbench-llm Workload And Scenario.

Configure The llm-d Deployment (Optional)

Sets deployment-identity fields for the workload's Helm release: the Inference Pool name Gateway API routes into, the target namespace, and the Helm chart version. Skip this call to accept the defaults execute_project_run provisions automatically on first run (a pool name derived from the workload code, default namespace). Chart version has no pinned default: leaving p_chart_version unset means helm upgrade --install runs with no --version flag, so Helm installs whatever the chart repository's latest release happens to be at deploy time. Set p_chart_version explicitly for a reproducible deployment.

curl -fsS -X POST "$METRUM_API_URL/rpc/upsert_llm_d_workload_config" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"p_account_id\":\"$METRUM_ACCOUNT_ID\",
\"p_workload_id\":1,
\"p_inference_pool_name\":\"deepseek-r1-vllm-multinode\",
\"p_gateway_namespace\":\"default\"
}" | jq

Parameters:

NameRequiredDescription
p_account_idYesAccount UUID; must match the authenticated caller.
p_workload_idYesWorkload numeric id. Must already be k8s_multinode_llm_d deployment mode.
p_inference_pool_nameYesName of the Gateway API Inference Extension InferencePool this workload's deployments register into.
p_gateway_namespaceNoKubernetes namespace for the Gateway/HTTPRoute. Defaults to default.
p_chart_versionNoPins the Helm chart version. Leaving it unset installs the chart repository's current latest release, not a validated default.
p_readiness_timeout_secondsNohelm upgrade --install --wait --timeout value. Defaults to 900.
p_cleanup_policyNouninstall (default, tears the release down) or retain (leaves it installed for debugging).

Run A Multi-Node Project

Launch with execute_project_run, not execute_scenario: pass no p_server_id/p_server_config_instance_id at all. Metrum resolves the target automatically to the cluster group's head server, forms the Kubernetes cluster across every group member if one is not already formed, and deploys the llm-d chart before any benchmark job starts.

curl -fsS -X POST "$METRUM_API_URL/rpc/execute_project_run" \
-H "Authorization: Bearer $METRUM_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"p_owner_account_id\":\"$METRUM_ACCOUNT_ID\",
\"p_project_name\":\"deepseek-r1-h200pair-multinode\",
\"p_run_name\":\"deepseek-r1-h200pair-multinode-run1\",
\"p_workload_code\":\"deepseek-r1-vllm-multinode\"
}" | jq

Parameters:

NameRequiredDescription
p_owner_account_idYesAccount UUID that owns the project.
p_project_nameYesProject created above.
p_run_nameYesDisplay name for this run.
p_workload_codeNo but recommendedRestricts the run to this workload when the project holds others.

Cluster formation (K3s join across every group member, then Gateway API/GAIE/ Agentgateway/LeaderWorkerSet controller bootstrap) is asynchronous and, on first use of a cluster group, can take several minutes. A cluster group that has already formed a cluster for a prior run reuses it.

Monitor A Multi-Node Run

Poll job status the same way as any other workload, via Get Job Status and List Jobs in Cloud Benchmark Execution. get_job_status reports the benchmark job's own status only; it has no visibility into cluster formation or the llm-d deployment, which run as a separate setup stage before the agent starts the job. Expect the job to report as not yet started for the full duration of that setup stage (several minutes on a cluster group's first use), then move to running once the deployment is ready and the benchmark actually begins.