Building tasks with TaskBuilder
TaskBuilder is the canonical interface for constructing and submitting kernel workloads through bloqade-core. It separates two responsibilities:
- the builder records which kernels to run, in what order, with which arguments, metadata, and shot counts;
- the device owns the language, serializer, supported dialects, and validation rules required by its backend.
The device combines those pieces when you call device.run_async(...).
Deprecated task factories
device.task(), device.batch_task(), and device.parameter_scan() are deprecated. They remain available for backwards compatibility, but new code should use TaskBuilder and device.run_async(builder, ...).
kernels ──add_subtask()──▶ TaskBuilder ──Device.run_async()──▶ Future ──result()──▶ Result
Basic usage
Create a builder, append each kernel invocation, and submit the builder through the device:
from bloqade import squin
from bloqade.core.device import Device, TaskBuilder
@squin.kernel
def bell():
q = squin.qalloc(2)
squin.h(q[0])
squin.cx(q[0], q[1])
device = Device(
context_name="my-context",
program_language="squin",
language_version="0.1.0",
)
builder = TaskBuilder()
subtask_index = builder.add_subtask(
bell,
num_shots=100,
metadata={"experiment": "bell", "basis": "Z"},
)
assert subtask_index == 0
# Validate, serialize, and print the task without contacting QLAM.
device.run_async(builder, dry_run=True)
# Finalize and submit the same builder.
future = device.run_async(builder, dry_run=False)
result = future.result(timeout=300)
print(result.shot_results())
Use a concrete subclass of Device to specify parameters used for validating the supplied kernels.
Add multiple subtasks
Every add_subtask() call adds one subtask at the end of the task. The return value is its zero-based index:
builder = TaskBuilder()
x_index = builder.add_subtask(x_kernel, num_shots=100)
y_index = builder.add_subtask(y_kernel, num_shots=200)
z_index = builder.add_subtask(z_kernel, num_shots=300)
assert (x_index, y_index, z_index) == (0, 1, 2)
A builder is mutable and append-only. Subtask order is submission order; there is currently no operation for removing or reordering existing entries.
Adding the same kernel object again creates another subtask but reuses its program entry:
builder.add_subtask(x_kernel, num_shots=25, metadata={"cycle": 0})
builder.add_subtask(x_kernel, num_shots=25, metadata={"cycle": 1})
The kernel is serialized once, while the two subtasks retain independent shot counts and metadata. Currently, program deduplication is based on kernel object identity; separately constructed kernel objects are treated as separate programs.
Kernel arguments and metadata
Pass kernel arguments after metadata, either positionally or by keyword. Keyword arguments are usually clearer:
builder.add_subtask(
parameterized_kernel,
100,
metadata={"sample": "A", "basis": "X"},
theta=0.25,
repetitions=3,
)
Arguments are validated against the kernel's Python signature and stored by parameter name. Metadata must be a JSON-serializable dictionary and is attached to the individual subtask. It can later be used to group or filter results.
Inspect and copy a builder
summary() shows program references, arguments, metadata, and shot counts. print_detailed() additionally includes each program's Kirin IR:
print(builder.summary())
print(builder.print_detailed())
Use copy() when several tasks share a common prefix:
baseline = TaskBuilder()
baseline.add_subtask(calibration_kernel, 100)
experiment = baseline.copy()
experiment.add_subtask(experiment_kernel, 1_000)
The mutable containers are copied, so appending to experiment does not change baseline. Kernel objects and existing subtask records are shared rather than deep-copied.
Finalization belongs to the device
When you pass in the TaskBuilder object to the device via Device.run_async(TaskBuilder, dry_run=...), the kernels will be validated, serialized, and a QLAM TaskDefinition will be created. If dry_run=True, a summary of the programs and subtasks will be printed.
The builder remains editable after either finalization or a dry run.
Migrating from the deprecated task factories
The deprecated factories all map to repeated add_subtask() calls. Replace a single-kernel device.task(...) call with one builder entry:
builder = TaskBuilder()
builder.add_subtask(
bell,
num_shots=100,
metadata={"experiment": "bell"},
)
device.run_async(builder, dry_run=True)
future = device.run_async(builder, dry_run=False)
result = future.result(timeout=300)
Existing callers can continue using device.task(), device.batch_task(), and device.parameter_scan() while migrating, but they should not be used in new code.
A batch is a builder containing different kernels:
builder = TaskBuilder()
for kernel, shots in zip(kernels, shot_counts):
builder.add_subtask(kernel, num_shots=shots)
future = device.run_async(builder, dry_run=False)
A parameter scan reuses the same kernel with different argument sets:
builder = TaskBuilder()
for parameter_set in parameter_sets:
builder.add_subtask(
parameterized_kernel,
num_shots=100,
**parameter_set,
)
future = device.run_async(builder, dry_run=False)
Unlike the three deprecated factories, the builder also supports mixing these patterns in one workload.
See Running kernels on a device for result filtering, frame selection, storage, groups, and future reconstruction.