Devices and placement
CUDA-Q Logical separates the machines that most stacks blur into one “backend” object. The separation is a firewall: each stage sees only its own structure, and a program can never branch on a later-stage cost fact it should not know.
Machines, devices, architectures
A logical machine (
@cudaq.logical.machine) is the P1 structure contract: regions, resource streams, capabilities, and plain integer capacities. No codes, no error rates:A logical machine with one capable region (examples/standalone/01_logical_placement.py).@cql.machine class TwoSlotMachine: compute = cql.architecture.region( capabilities=( cql.architecture.capability.logical_compute, cql.architecture.capability.logical_measurement, ), capacity=2, )
Capabilities are typed keys in an open
cudaq.logical.machinevocabulary; the compiler itself interpretslogical_compute,logical_measurement,logical_factory, andresource_transfer.A QEC machine carries the P2 refinement: encoded block pools, one selected encoding per region, and block capacities.
A physical machine carries the P3 refinement: physical resources, native operations and instruments, bindings, routing, and timing assumptions.
An immutable device binds adjacent machines explicitly. A device with only a logical machine is complete for placement work; binding an encoding per region grows it into a QEC device, and physical bindings complete a P3 device.
For homogeneous cases you never write the normalized containers explicitly. The device builder separates logical region facts from the vertical encoding binding:
import cudaq.logical as cql
builder = cql.devices.DeviceBuilder("SteaneMemory")
memory = builder.logical.add_memory(capacity=8)
builder.qec.bind(memory, encoding=cql.codes.Steane)
SteaneMemory = builder.build()
assert SteaneMemory.layers == (cql.stages.P1, cql.stages.P2)
builder.qec.bind(...) sets the QEC refinement of a logical region; the
singular name is deliberate — each region has one selected encoding.
Stop at the layer your study needs
cudaq.logical.devices.DeviceBuilder is progressively complete. build() does
not demand code facts that the requested compiler stage cannot use:
Declared layers |
Builder boundary |
Suitable work |
|---|---|---|
P1 |
|
logical placement, capacities, resource supply |
P1 + P2 |
|
code selection, gadgets, static/analytical estimates |
P1 + P2 + P3 |
|
physical lowering, scheduling, schedule estimates |
Adding the physical layer is the same shape as adding the QEC layer. Declare the carriers and what they can natively do, bind the encoded region onto them, and state an operating point:
carriers = builder.physical.add_qubits(
surface_code.block.size,
native_actions=cql.architecture.physical_actions.clifford_set(),
native_instruments=(
cql.architecture.physical_instruments.MZ,
cql.architecture.physical_instruments.MPP,
),
)
builder.physical.bind(encoded_region, to=carriers)
builder.physical.set_operating_point(timing={"cycle_ns": 1.0})
physical.bind(encoded, to=carriers) is the vertical ownership boundary: the
QEC region stays a P2 fact and the carrier allocation stays a P3 fact.
set_operating_point turns dimensionless cycle counts into time. The compiler
derives the event graph and the schedule from there; you do not hand-write
either.
A placement-only device is therefore complete as written:
logical = cql.devices.DeviceBuilder("LogicalPlacement")
logical.logical.add_compute(capacity=64)
LogicalPlacement = logical.build()
assert LogicalPlacement.layers == (cql.stages.P1,)
The stack views contain only the layers actually declared, so a P1-only device
cannot accidentally display P2 block pools. The builder infers common member
labels: add_compute, add_memory, and add_factory attach typed
capabilities and generate region and resource-stream labels with suffixes as
needed. DeviceBuilder itself always has an explicit stable name.
Ready-made complete device stacks also ship as compilation targets — the
surface_physical_target(distance=3, logical_capacity=1) stack in
examples/02_surface_code_resource_estimate.py binds a rotated-surface-code
QEC machine and a physical machine under a logical machine without any
hand-written builder code.
Automatic placement: constraints, preferences, witnesses
Placement solves hard constraints and soft preferences over the machine, and the solution is inspectable evidence, not solver state:
@cql.program
def bell() -> tuple[bool, bool]:
qubits = cql.allocate(2, state=cql.types.zero, name="data")
qubits[0] = cql.h(qubits[0])
qubits[0], qubits[1] = cql.cx(qubits[0], qubits[1])
return cql.measure_z(qubits[0]), cql.measure_z(qubits[1])
# %%
# Compile and place the logical owners into the machine's compute region.
logical = cql.compile(bell)
placed = cql.compiler.place(
logical,
device=TwoSlotMachine,
placement=(cql.architecture.colocate(logical.values.data),),
)
assert placed.stage == cql.stages.P1
assert {binding.space for binding in placed.placement.bindings} == {"compute"}
assert {binding.slot for binding in placed.placement.bindings} == {0, 1}
assert cql.compiler.Build.replay(
placed.serialize()).placement == placed.placement
place materializes P0 before invoking the selector. Address values through
its typed view — p0.values.data for a named allocation group, p0.values[0]
for structural ordinals — and they resolve immediately to LogicalValueRefs;
Python variable names are diagnostics, never placement identity. Pass an
existing P0 Build only when you intentionally inspected or reused it.
The witness records every binding, the machine, the objective, any soft preference the solver had to give up, and the deterministic tie-break:
p1.placement.bindings— onePlacementBindingper logical owner, naming its region and slot;p1.placement.objectiveandp1.placement.relaxed_preferences— what was optimized and what was surrendered;cudaq.logical.compiler.Build.replay(p1.serialize()).placement == p1.placement— the witness is part of the immutable, replayable build.
Hard constraints are hard. Requiring a capability that no region provides fails closed:
try:
# ValueError: no machine space satisfies the placement constraints
cql.compiler.place(
p0,
device=TwoSlotMachine,
placement=(cql.architecture.require_capability(
cql.architecture.capability.logical_factory),),
)
except ValueError as exc:
assert "no machine space satisfies the placement constraints" in str(exc)
Placement never silently relaxes a hard requirement. Soft preferences, by contrast, may be surrendered — and the surrender is reported:
p1 = cql.compiler.place(
p0,
device=TwoSlotMachine,
placement=(
cql.architecture.colocate(p0.values.data),
cql.architecture.prefer(
space=TwoSlotMachine.compute,
for_=cql.architecture.lifecycle.ACTIVE,
),
),
objective=cql.architecture.metric.expected_spacetime_volume,
)
assert p1.placement.relaxed_preferences == ()
The constraint vocabulary is cudaq.logical.architecture: colocate,
allow_spaces, require_capability, prefer, and local for exact-slot
pinning. Exact slots are singleton constraints, not a verification bypass —
hand-authored placements produce the same verified witness as solved ones.
Code-agnostic P1
P1 placement never selects an encoding.
cudaq.logical.architecture.colocate(p0.values.data) keeps several logical
owners in one logical region, but each owner consumes a distinct P1 slot, and a
PlacementBinding carries no encoding field. Encodings enter at P2 — bound
per region through DeviceBuilder.qec.bind(...) or carried by a compilation
target — and the P2 witness is separate from p1.placement: changing the
encoding does not rewrite the verified P1 artifact.
Where the firewall pays off
Placement constraints speak machine vocabulary — regions, slots, capabilities —
so a portable program re-places when you swap device=, and you can reuse one
machine across many programs. Capacities and capabilities live on the machine;
encodings live on the device binding. So when a study moves from one machine to
another, neither the program nor the estimate code changes.
Continue from here
Magic states and protocols — typed resource supply and a concrete 15-to-1 factory.
Logical Clifford+T synthesis — legalizing logical rotations before placement.
Examples — the placement example in the context of the full shipped set.