Skip to main content

Cluster config explained

Every simulation in LLMServingSim is driven by one JSON file: a cluster config. It captures the entire hardware topology, how many nodes, how many instances per node, what GPU each instance runs on, how memory is laid out, and how the model is parallelized.

Once you understand this file, every example in this section is a small variation on the same shape.

The minimum viable config

This is configs/cluster/single_node_single_instance.json: the smallest config that runs:

configs/cluster/single_node_single_instance.json
{
"num_nodes": 1,
"link_bw": 16,
"link_latency": 20000,
"nodes": [
{
"num_instances": 1,
"cpu_mem": {
"mem_size": 512,
"mem_bw": 256,
"mem_latency": 0
},
"instances": [
{
"model_name": "meta-llama/Llama-3.1-8B",
"hardware": "RTXPRO6000",
"npu_mem": {
"mem_size": 96,
"mem_bw": 1597,
"mem_latency": 0
},
"num_npus": 1,
"tp_size": 1,
"pd_type": null
}
]
}
]
}

That's: one node, one instance, running Llama-3.1-8B on one RTXPRO6000 GPU with TP=1 (no parallelism).

The file has three nested levels. We'll walk through them top-down.

1. Top level, the cluster

{
"num_nodes": 1,
"link_bw": 16,
"link_latency": 20000,
"nodes": [...]
}
FieldTypeMeaning
num_nodesintNumber of physical nodes in the cluster
link_bwfloat or float[]ASTRA-Sim topology link bandwidth in GB/s
link_latencyfloat or float[]ASTRA-Sim topology link latency in ns
nodesarrayOne entry per node (length must match num_nodes)

For multi-node setups (e.g., two boxes in a rack), set num_nodes: 2 and add a second node entry. link_bw / link_latency can be either:

  • a scalar, broadcast to every ASTRA-Sim topology dimension
  • an array, with one value per final network.yml::npus_count dimension

Optional top-level fields:

FieldUsed for
cxl_memCXL memory expansion config (see CXL memory tier)

2. Per-node level

{
"num_instances": 1,
"cpu_mem": {
"mem_size": 512,
"mem_bw": 256,
"mem_latency": 0
},
"instances": [...]
}
FieldTypeMeaning
num_instancesintHow many serving instances live on this node
cpu_mem.mem_sizefloatHost CPU memory capacity (GB)
cpu_mem.mem_bwfloatCPU memory bandwidth (GB/s)
cpu_mem.mem_latencyfloatCPU memory latency (ns)
instancesarrayOne entry per instance (length = num_instances)

Optional per-node fields:

FieldUsed for
cpu_mem.pim_configName of a PIM device config in configs/pim/ (see PIM attention offload)
powerPower model coefficients (see Power modeling)

3. Per-instance level

This is where the real work happens. An instance is one independent LLM serving replica, a model, a parallelism strategy, and a chunk of GPUs.

{
"model_name": "meta-llama/Llama-3.1-8B",
"hardware": "RTXPRO6000",
"npu_mem": {"mem_size": 96, "mem_bw": 1597, "mem_latency": 0},
"num_npus": 1,
"tp_size": 1,
"pd_type": null
}

Required fields

FieldTypeMeaning
model_namestringHugging Face model id. Must match a config in configs/model/{model_name}.json
hardwarestringHardware tag. Must match profiler/perf/{hardware}/ (e.g. RTXPRO6000, H100)
npu_memobjectPer-GPU memory: mem_size (GB), mem_bw (GB/s), mem_latency (ns)
pd_typestring|null"prefill", "decode", or null for combined prefill+decode

Parallelism fields (at least one required)

FieldTypeDefaultMeaning
num_npusintinferredTotal GPUs for this instance, equals tp_size * pp_size
tp_sizeintinferredTensor-parallel degree
pp_sizeint1Pipeline-parallel degree
ep_sizeinttp_size (MoE) / 1 (dense)Expert-parallel degree
dp_groupstring|nullnullInstances with the same string form one data-parallel group, wave-synchronized per iteration; for MoE they also share experts across the group

You only need to provide one of num_npus or tp_size. The other gets inferred. So:

  • tp_size: 4num_npus = 4 * pp_size (PP defaults to 1, so 4)
  • num_npus: 4, pp_size: 2tp_size = 2

Parallelism rules to remember:

  • num_npus == tp_size * pp_size (always)
  • pp_size <= num_hidden_layers: stages are cut on transformer-block boundaries, so a stage cannot be empty
  • TP and EP share the same GPUs: dense layers do TP-ALLREDUCE, MoE layers do EP-ALLTOALL
  • Without dp_group: ep_size <= tp_size
  • With dp_group: EP can scale beyond a single instance's GPUs (see DP+EP example)
  • For MoE models: ep_size must divide num_local_experts

Optional advanced fields

FieldUsed for
placementPer-layer / per-block weight + KV-cache placement (see CXL memory)
any of the 14 runtime overridesPer-instance scheduler / memory / dtype settings (see below)

Per-instance runtime overrides

Everything so far describes hardware. An instance can also carry runtime settings, and that is what makes heterogeneous clusters possible: 14 of the python -m serving flags can be re-specified per instance, so a prefill instance and a decode instance in the same run can be scheduled completely differently.

The precedence is one level deep:

instances[i].<field> > --<field> on the CLI > built-in default

A field written in the instance wins for that instance. Every other instance keeps whatever the CLI said. There is no merging and no inheritance between instances.

The 14 overridable fields

FieldOverridesTypical reason to set it per instance
max_num_seqs--max-num-seqsNarrow batches on a prefill instance, wide ones on decode
max_num_batched_tokens--max-num-batched-tokensBig prefill chunks, small decode steps
long_prefill_token_threshold--long-prefill-token-thresholdStop one long prompt monopolising a prefill instance
block_size--block-sizeCoarser blocks where fragmentation matters less
dtype--dtypeServe the same model at two precisions
kv_cache_dtype--kv-cache-dtypeFP8 KV on the decode instance only
enable_chunked_prefill--enable-chunked-prefillChunk on prefill, never on decode
enable_prefix_caching--enable-prefix-cachingCache where prefixes repeat, skip where they don't
npu_mem.mem_util--npu-memory-utilizationLeave headroom on one instance only
reserve_full_isl--reserve-full-islLoosen admission on a decode-only instance
enable_local_offloading--enable-local-offloadingWeight offload on one instance
enable_attn_offloading--enable-attn-offloadingPIM attention on one instance
enable_sub_batch_interleaving--enable-sub-batch-interleavingOverlap NPU and PIM on the offloading instance
enable_block_copy--enable-block-copyFaithful per-layer expert variance on one instance

Note where mem_util sits: it is the only override nested inside another object, because it exists to scale npu_mem.mem_size and follows that block's mem_* naming.

"npu_mem": {"mem_size": 96, "mem_bw": 1597, "mem_latency": 0, "mem_util": 0.8}

A heterogeneous example

configs/cluster/single_node_heterogeneous.json runs one Qwen3-32B prefill instance and one decode instance, both TP=2, with opposite scheduler settings:

configs/cluster/single_node_heterogeneous.json (instances only)
"instances": [
{
"model_name": "Qwen/Qwen3-32B",
"hardware": "RTXPRO6000",
"npu_mem": {"mem_size": 96, "mem_bw": 1597, "mem_latency": 0},
"num_npus": 2,
"tp_size": 2,
"pd_type": "prefill",
"max_num_seqs": 32,
"max_num_batched_tokens": 8192,
"enable_chunked_prefill": true,
"enable_prefix_caching": true
},
{
"model_name": "Qwen/Qwen3-32B",
"hardware": "RTXPRO6000",
"npu_mem": {"mem_size": 96, "mem_bw": 1597, "mem_latency": 0},
"num_npus": 2,
"tp_size": 2,
"pd_type": "decode",
"max_num_seqs": 256,
"max_num_batched_tokens": 0,
"enable_chunked_prefill": false,
"enable_prefix_caching": false
}
]

Reading it: the prefill instance takes few sequences with a large token budget, because a prefill step wants long chunks. The decode instance takes many sequences and no token cap, because each decode step contributes one token per sequence.

configs/cluster/single_node_pd_per_instance_config.json is the Llama-3.1-8B version of the same idea, and additionally overrides long_prefill_token_threshold, block_size, dtype, and kv_cache_dtype.

max_num_batched_tokens: 0 is not infinity

0 maps to "unlimited", but the scheduler then applies min(max_num_batched_tokens, max_position_embeddings). On Qwen3-32B that is 40960, so the decode instance above runs at 40960, not unbounded. See Model config.

What you cannot set per instance

The other 15 flags are cluster-wide. Writing one inside an instance object does nothing at all: no key reads it, and no error is raised.

ScopeFlags
Cluster / backend--cluster-config, --network-backend
Router, cross-instance by definition--request-routing-policy, --expert-routing-policy
Shared lower KV tier--enable-prefix-sharing, --prefix-storage
Workload, one per run--dataset, --num-reqs, --skip-prefill
Run plumbing--output, --run-id, --inputs-root, --save-trace-text, --keep-inputs, --log-interval, --log-level

Two rejected combinations

Both are checked per instance, against the effective values, at config-load time:

  • enable_sub_batch_interleaving without enable_attn_offloading - there is nothing to overlap the NPU sub-batch against.
  • enable_sub_batch_interleaving with pp_size > 1: an interleaved trace leaves both sub-batches mid-block at every stage edge, so a pipeline stage has no single hidden state to hand on.

Full semantics, including dtype's three-level fallback and the mem_util range check, are in Reference → Cluster config.

DP+EP, the topology that needs more explanation

When multiple instances share the same dp_group, they form a multi-dimensional ASTRA-Sim topology, innermost dimension first: [tp_size, dp_group_size], or [tp_size, pp_size, dp_group_size] when pp_size > 1. Collectives are scoped per dimension:

  • TP ALLREDUCE runs on the TP dim only (within an instance)
  • EP runs on the DP dim, plus the TP dim when EP spans past one instance's GPUs. The PP dim is never involved — vLLM's EP group pins the pipeline stage.

All instances in a DP group share one ASTRA-Sim process with wave-synchronized scheduling. MoE expert weights are sharded by ep_size: each instance holds num_local_experts / ep_size experts.

Concrete example: Qwen3-30B-A3B has 128 experts. With tp_size=1, ep_size=2, dp_group="A" and two instances, each holds 64 experts. Per-token activation crosses the DP group via ALLTOALL.

This is the DP+EP MoE example.

What config_builder.py does with this file

When you launch the simulator, serving/core/config_builder.py reads the cluster config and generates three ASTRA-Sim input files under astra-sim/inputs/:

Generated fileDriven by
network/network.ymllink_bw, link_latency, [tp_size, (pp_size,) dp_group_size] topology
system/system.jsonMemory bandwidths, scheduling policy, per-dim collective implementations
memory/memory_expansion.jsonCXL devices and any extended memory tiers

You don't write these by hand, they're regenerated on every run from the cluster config.

Provided configs

The repo ships 27 worked configs under configs/cluster/. Those without a link are runnable as-is but have no dedicated example page:

ConfigShapeUsed by
single_node_single_instance.json1 node, 1 instance, Llama-3.1-8B, TP=1the default; Tensor parallel raises it to tp_size=2
single_node_single_instance_H100.jsonLlama-3.1-70B on H100, TP=4
single_node_multi_instance.json1 node, 2 instancesMulti-instance LOAD routing
single_node_4_instance_2TP.json1 node, 4 instances at TP=2
single_node_heterogeneous.jsonP/D pair with opposite runtime settingsPer-instance runtime overrides
single_node_pd_instance.jsonprefill/decode disaggregationPrefill/decode split
single_node_pd_per_instance_config.jsonP/D with per-instance runtime limitsPer-instance runtime overrides
single_node_pp_instance.json4 GPUs as pp=4Pipeline parallel
single_node_tp_pp_instance.json4 GPUs as tp=2 x pp=2Pipeline parallel
single_node_moe_single_instance.jsonQwen3-MoE, TP=2 EP=2Expert parallel
single_node_moe_dp_ep_instance.json2 MoE instances in one DP group, EP=2DP+EP MoE
single_node_dp_instance.jsonDP=2 x TP=2, dense model, 4 GPUs
rtx4090_single_instance.jsonRTX 4090, Llama-3.1-8B TP=1, mem_util calibratedValidation
rtx4090_tp2_instance.json2x RTX 4090 as tp=2, Llama-3.1-8B. Needs an RTX4090 tp=2 profile first — only tp1 is bundled, so it raises FileNotFoundError as shippedProfiler
rtx4090_multi_instance.json2 independent TP=1 RTX 4090 instances
single_node_moe_dp_tp_instance.jsonDP=2 x TP=2 MoE, EP=2, 4 GPUs
single_node_moe_dp_pp_instance.jsonDP=2 x PP=2 MoE, EP=2, 4 GPUs
single_node_moe_dp_tp_pp_instance.jsonDP=2 x TP=2 x PP=2 MoE, EP=4, 8 GPUs
single_node_moe_multi_instance.json2 MoE instances, no DP group
single_node_moe_pd_instance.jsonMoE with P/D disaggregation
single_node_moe_pp_instance.jsonMoE on 4 GPUs, tp=2 x pp=2, ep=2Pipeline parallel
single_node_cxl_instance.jsonCXL memory expansionCXL memory tier
single_node_memory_instance.jsonweight / KV placement controlCXL memory tier
single_node_pim_instance.jsonPIM-enabled memory + power modelPIM attention offload
single_node_power_instance.jsonpower modeling enabledPower modeling
dual_node_multi_instance.json2 nodes, 2 instances eachmulti-node setups
dual_node_moe_dp_ep_intra_inter_instance.json2-node DP+EP MoE, per-dimension link_bw / link_latencyDP+EP MoE

What's next

Now that you can read a cluster config, pick an example and see how the same shape produces very different topologies: