Skip to main content

ClusterBuilder

ClusterBuilder is the primary API for composing clusters from multiple providers. It follows the builder pattern: chain .add() calls, finish with .build().


Basic Usage

from network.node_providers import ClusterBuilder
from network.distributed_qvm import DistributedQVM, QVMMode

registry = (
ClusterBuilder()
.add("local_gpu_aer", n_nodes=8, max_qubits=28)
.add("local_gpu_mps", n_nodes=4, max_qubits=1000)
.build()
)

qvm = DistributedQVM(registry, mode=QVMMode.EMULATED)

.add(provider_name, **kwargs)

Adds a provider to the cluster plan. Returns self for chaining.

ParameterTypeDescription
provider_namestrKey from NodeProviderRegistry.list() or a custom registered name
**kwargsanyPassed directly to NodeProvider.build_nodes() — provider-specific
builder = ClusterBuilder()

# Each .add() returns the same builder
builder = (
builder
.add("local_gpu_aer", n_nodes=4, max_qubits=28) # 4 SV nodes
.add("local_gpu_mps", n_nodes=2, max_qubits=500) # 2 MPS nodes
.add("lumi_amd_gpu", n_nodes=2, gpus_per_node=8)# LUMI nodes
)

Providers are not instantiated until .build() is called — .add() only records the intent.


.build() → QPURegistry

Executes the plan:

  1. For each slot: instantiate the provider class
  2. Call provider.health_check() — warns if it fails (but continues)
  3. Call provider.build_nodes(**kwargs) → get List[QPUNodeSpec]
  4. Register all specs in a QPURegistry

Returns a QPURegistry ready to pass to DistributedQVM.

registry = builder.build()

# Inspect the result
report = registry.status_report()
print(f"Nodes: {report['total_nodes']}")
print(f"Qubits: {report['total_physical_qubits']}")
Fallback

If all providers fail, build() falls back to QPURegistry.default_simulation_cluster() and logs a warning.


.describe() → str

Prints the planned cluster without building it — useful for debugging:

plan = (
ClusterBuilder()
.add("lumi_amd_gpu", n_nodes=4, gpus_per_node=8)
.add("local_gpu_mps", n_nodes=8, max_qubits=2000)
.add("azure_quantum", targets=["rigetti.qpu.ankaa-3"])
.describe()
)
print(plan)
ClusterBuilder plan:
[1] lumi_amd_gpu (LUMI AMD MI250X (ROCm))
kwargs: n_nodes=4, gpus_per_node=8
[2] local_gpu_mps (Local GPU — Aer MPS (tensor-network))
kwargs: n_nodes=8, max_qubits=2000
[3] azure_quantum (Azure Quantum)
kwargs: targets=['rigetti.qpu.ankaa-3']

Provider kwargs reference

local_cpu

.add("local_cpu",
n_nodes=4, # number of virtual CPU nodes
max_qubits=25, # max qubits per node (statevector, RAM-limited)
)

local_gpu_aer

.add("local_gpu_aer",
n_nodes=8, # number of CUDA statevector nodes
max_qubits=28, # safe default: 28q = 4GB, 32q = 64GB RAM
)
Memory limit

Statevector RAM usage = $2^n \times 16$ bytes. At 32q this is 64 GB — ensure your machine has enough RAM or use MPS.

local_gpu_mps

.add("local_gpu_mps",
n_nodes=4, # number of MPS simulation nodes
max_qubits=2000, # can handle thousands of qubits (low entanglement)
bond_dim=512, # MPS bond dimension (higher = more accurate, slower)
)

local_gpu_cuquantum

.add("local_gpu_cuquantum",
n_nodes=1, # typically 1 per physical GPU
max_qubits=34, # A100 80GB: safe up to 34q exact
)

Requires: pip install cupy-cuda12x

lumi_amd_gpu

.add("lumi_amd_gpu",
n_nodes=4, # LUMI compute nodes
gpus_per_node=8, # MI250X GCDs per node
max_qubits_sv=34, # statevector per GCD
max_qubits_mps=2000, # MPS per node
lumi_host="lumi", # SSH alias in ~/.ssh/config
lumi_project="project_465002463", # LUMI project ID
)

See LUMI GPU Guide.

azure_quantum

.add("azure_quantum",
targets=[
"rigetti.qpu.ankaa-3", # 84q, $0.0009/shot — cheapest
"quantinuum.qpu.h2-1", # 56q, $0.000095/HQC — best fidelity
"ionq.qpu.forte-1", # 35q, $0.00975/shot
"microsoft.estimator", # free, resource estimation
],
resource_id="...", # Azure Quantum workspace resource ID
location="eastus",
subscription_id="...",
)

See Azure Quantum Guide.

ibm_quantum

.add("ibm_quantum",
backends=["ibm_torino", "ibm_kyoto"], # IBM backend names
token="your-ibm-token",
channel="ibm_quantum", # or "ibm_cloud"
)

aws_braket

.add("aws_braket",
devices=["rigetti", "iqm_garnet"], # Braket device names
aws_region="us-east-1",
aws_access_key="...",
aws_secret_key="...",
s3_bucket="your-braket-bucket",
)

custom_rest

.add("custom_rest",
endpoints=[
"http://gpu-node-01.internal:8080",
"http://gpu-node-02.internal:8080",
],
max_qubits=40, # qubits each endpoint can handle
api_key="secret", # sent as Bearer token
timeout_s=120.0, # request timeout
)

See Custom Providers for the endpoint contract.


Convenience Factories

Instead of ClusterBuilder, use these one-liners:

from network.node_providers import (
make_local_cluster,
make_lumi_cluster,
make_azure_cheap_cluster,
make_hybrid_cluster,
)

# Zero-cost local simulation
registry = make_local_cluster(
n_cpu_nodes=0,
n_gpu_sv_nodes=8,
n_gpu_mps_nodes=4,
sv_max_qubits=28,
mps_max_qubits=500,
)

# LUMI-first
registry = make_lumi_cluster(
n_nodes=8,
gpus_per_node=8,
lumi_host="lumi",
include_local_fallback=True,
)

# Azure cheapest QPU
registry = make_azure_cheap_cluster(
azure_resource_id="...",
include_local_fallback=True,
)

# Full hybrid
registry = make_hybrid_cluster(
lumi_nodes=4,
azure_resource_id="...",
ibm_token="...",
n_local_sv=8,
n_local_mps=4,
)

Shard Strategies

Once you have a registry, control how the ShardPlanner assigns shards:

from network.distributed_qvm import DistributedQVM, ShardStrategy, QVMMode

qvm = DistributedQVM(
registry,
mode=QVMMode.HYBRID,
strategy=ShardStrategy.GPU_FIRST, # or BALANCED, QPU_FIRST, NOISE_AWARE
max_wire_cuts=16,
shots=2048,
)
StrategyBehavior
BALANCEDAssign largest-capacity nodes first (default)
GPU_FIRSTPrefer GPU simulators over QPUs (lower latency)
QPU_FIRSTPrefer real QPUs over simulators
NOISE_AWAREAssign deepest circuits to lowest-noise nodes
GREEDYFill nodes in registration order

Example: Scaling from laptop to supercomputer

import os
from network.node_providers import ClusterBuilder
from network.distributed_qvm import DistributedQVM, QVMMode, ShardStrategy

LUMI_NODES = int(os.environ.get("LUMI_NODES", "0"))
AZURE_RID = os.environ.get("AZURE_QUANTUM_RESOURCE_ID", "")

registry = (
ClusterBuilder()
# LUMI: only added if LUMI_NODES > 0
*([] if LUMI_NODES == 0 else [
("lumi_amd_gpu", {"n_nodes": LUMI_NODES, "gpus_per_node": 8}),
])
)

# Simpler conditional approach:
b = ClusterBuilder()
if LUMI_NODES > 0:
b = b.add("lumi_amd_gpu", n_nodes=LUMI_NODES, gpus_per_node=8)
if AZURE_RID:
b = b.add("azure_quantum",
targets=["rigetti.qpu.ankaa-3"],
resource_id=AZURE_RID)

# Always add local fallback
b = (
b
.add("local_gpu_mps", n_nodes=8, max_qubits=1000)
.add("local_gpu_aer", n_nodes=4, max_qubits=28)
)

registry = b.build()
qvm = DistributedQVM(registry, mode=QVMMode.HYBRID, strategy=ShardStrategy.GPU_FIRST)

print(b.describe())
print(registry.status_report())

This single codebase works on a laptop (local only), a server with LUMI access, or a full hybrid cloud — driven purely by environment variables.