Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions docs/guides/chunking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
Chunks and auto-chunking
========================

This guide explains how the executor breaks a :class:`~.QuantumProgram` into **chunks** for
execution on quantum hardware. At the end of this guide, you will understand:

* What a chunk is and how it relates to a shot loop.
* Why chunk sizes matter for performance and noise averaging.
* How the auto-chunking heuristic selects chunk sizes for you.
* How system limits constrain chunk sizes.
* When and how to set chunk sizes manually.
* What happens when chunk sizes do not evenly divide the work.
* How to inspect chunk timing in job results.

What is a chunk?
----------------

When the executor runs a :class:`~.QuantumProgram`, it does not necessarily execute every circuit
configuration at once. Instead, it groups circuits into **chunks** and executes one chunk per
**shot loop**. A shot loop is a single round of execution on the quantum hardware, in which every
circuit in the chunk is run for the requested number of shots.

The ``chunk_size`` of an item inside of a quantum program controls how many bound circuits from that
item appear in each shot loop. Each bound circuit in the chunk receives a different set of parameter
values, either supplied directly by the user or generated by a samplex.

For example, suppose you have a parametric circuit with 100 different parameter configurations.
If the chunk size for that item is 20, the executor will need at least 5 shot loops to process
all configurations, with 20 bound copies of the circuit per loop.

A static (non-parametric) circuit is the special case where there is exactly one trivial
parameter set and a chunk size of 1.

.. code-block:: python

from qiskit_ibm_runtime.quantum_program import QuantumProgram

program = QuantumProgram(shots=1024)

# The chunk_size parameter is available on both append methods.
# Leave it as None (the default) to use auto-chunking.
program.append_circuit_item(isa_circuit, circuit_arguments=values, chunk_size=None)

Why chunk sizes matter
----------------------

Chunk sizes directly affect how efficiently the quantum hardware is utilized:

- **Too small:** Each shot loop contains few circuits, which means more shot loops are needed and
the hardware spends proportionally more time on overhead rather than useful computation.

- **Too large:** A shot loop packed with too many circuits can exceed the internal capabilities of
the system.

Finding the right balance is important. The executor provides **auto-chunking** to handle this
automatically, so that you get good performance without needing to reason about the hardware
constraints yourself.

Auto-chunking
-------------

Auto-chunking is the default behavior when ``chunk_size`` is left as ``None``. The executor
uses a server-side heuristic that attempts to fill each shot loop with as many circuits as
possible without exceeding any system limits.

The heuristic follows two principles:

1. **Proportional sizes.** Chunk sizes are proportional to the sizes of the program items. If one
item has 10 times as many configurations as another, its chunk size will be roughly 10 times
larger. This ensures that all items contribute to each shot loop in proportion to their
workload, which is important when circuits are meant to be randomized together (for example,
for noise averaging).

2. **Maximize utilization.** Subject to the proportionality rule, the heuristic scales up chunk
sizes as much as possible before hitting any system limit. This minimizes the total number of
shot loops.

How the heuristic works
~~~~~~~~~~~~~~~~~~~~~~~

Consider a program with items whose sizes are :math:`s_1, s_2, \ldots, s_n`. The heuristic
proceeds in two steps:

1. Compute the **base chunk sizes** by normalizing to the smallest item:

.. math::

k_i^{\text{base}} = \left\lfloor \frac{s_i}{\min(s)} \right\rfloor

This gives the smallest chunk sizes that respect the proportionality rule. The smallest item
gets a chunk size of 1, and every other item gets a chunk size that reflects its relative size.

2. Find the largest **multiplier** :math:`c` such that multiplying all base chunk sizes by
:math:`c` still satisfies every system limit:

.. math::

k_i = c \cdot k_i^{\text{base}}

The multiplier :math:`c` is chosen to be as large as possible while keeping all cost functions
within bounds (see :ref:`validation-and-limits` below).

.. _chunking-example:

Example
~~~~~~~

Suppose you have a program with two items: item A has 100 configurations and item B has 300
configurations. Imagine the system has a limit of 50 total circuits per shot loop.

.. list-table::
:header-rows: 1
:widths: 25 25 25 25

* - Item
- Size
- Base chunk size
- Final chunk size (c=12)
* - A
- 100
- 1
- 12
* - B
- 300
- 3
- 36

The total circuits per shot loop would be :math:`12 + 36 = 48`, which fits within the limit of
50. The next multiplier, :math:`c = 13`, would give :math:`13 + 39 = 52`, which exceeds the limit.

.. note::

The proportionality constraint means that items with very different sizes can run into limits.
For example, if the circuit-count limit is 100 and one item has size 1000 while another has
size 1, the base chunk sizes would be 1 and 1000, totaling 1001 circuits, which already
exceeds the limit. In cases like this, you should split the items into separate jobs.

.. _validation-and-limits:

Validation and limits
---------------------

Before execution begins, the system checks that a set of chunk sizes is valid by evaluating
**cost functions** against **system limits**. A typical cost function adds up a quantity across
all circuits in a shot loop, for example:

- **Instruction count:** The total number of a specific gate type (such as CZ gates) across all
circuits in the shot loop.
- **Circuit count:** The total number of circuits in the shot loop.

Each cost function has a corresponding limit. The chunks are valid if and only if every cost
function stays within its limit:

.. math::

F_j(C, k) \leq L_j \quad \text{for all } j = 1, \ldots, m

where :math:`C` represents the circuits, :math:`k` represents the chunk sizes, and :math:`L_j`
are the system limits.

If auto-chunking is in use, the heuristic is designed to always produce valid chunk sizes
(provided the base chunk sizes themselves are valid). If you set chunk sizes manually, the system
will reject programs whose chunks exceed any limit.

When can you set chunk sizes manually?
--------------------------------------

Manual control over chunk sizes is available **only when running in a session**. In a session,
you pay for wall-clock time and have the flexibility to make your own performance trade-offs.

- **In a session:** Your ``chunk_size`` values are respected (as long as they pass validation).
This lets you control the exact composition of each shot loop, for example, to ensure specific
randomization patterns for noise averaging.
- **Outside a session (job mode and batch mode):** The auto-chunking heuristic is always used,
and any ``chunk_size`` values you set are ignored. This ensures efficient system utilization
in shared-access scenarios.

Auto-chunking is the default in all execution modes.

.. code-block:: python

from qiskit_ibm_runtime import Executor, Session

# In a session, manual chunk sizes are respected
with Session(backend=backend) as session:
executor = Executor(session)
program = QuantumProgram(shots=1024)
program.append_circuit_item(isa_circuit, circuit_arguments=values, chunk_size=25)
job = executor.run(program)

# Outside a session, chunk_size is ignored and auto-chunking is used
executor = Executor(backend)
program = QuantumProgram(shots=1024)
program.append_circuit_item(isa_circuit, circuit_arguments=values, chunk_size=25) # ignored
job = executor.run(program)

Remainder handling
------------------

Chunk sizes do not always evenly divide the total number of configurations in a program item.
When this happens, the executor **front-loads** work: early shot loops are filled to the full
chunk size, and any remaining configurations appear in the final shot loops.

If a program has multiple items with different amounts of remaining work, some items may run out
of configurations before others. When an item has no more work left, its bound circuits simply
do not appear in the subsequent shot loops. This means that the final shot loops of a job can
contain fewer circuits than earlier ones.

Example
~~~~~~~

Consider a program with two items: item A has 50 configurations with a chunk size of 20, and
item B has 30 configurations with a chunk size of 10.

.. list-table::
:header-rows: 1
:widths: 20 40 40

* - Shot loop
- Item A circuits
- Item B circuits
* - 1
- 20
- 10
* - 2
- 20
- 10
* - 3
- 10
- 10
* - 4
- (none)
- (none)

Item A requires 3 shot loops (20 + 20 + 10), and item B also requires 3 (10 + 10 + 10). Both
items finish at the same time in this case. Now consider what happens if item B had 40
configurations instead:

.. list-table::
:header-rows: 1
:widths: 20 40 40

* - Shot loop
- Item A circuits
- Item B circuits
* - 1
- 20
- 10
* - 2
- 20
- 10
* - 3
- 10
- 10
* - 4
- (none)
- 10

Item A finishes after shot loop 3, but item B still has 10 configurations left. Shot loop 4
contains only item B's circuits. You can verify this kind of behavior by inspecting the
``chunk_timing`` metadata described in the next section.

Chunk timing
------------

After a job completes, you can inspect how the program was chunked and how long each chunk took
to execute. This information is available in the result's
:class:`~.qiskit_ibm_runtime.quantum_program.Metadata` object via the ``chunk_timing`` attribute,
which contains a list of :class:`~.qiskit_ibm_runtime.quantum_program.ChunkSpan` entries, one per
executed chunk.

Each :class:`~.qiskit_ibm_runtime.quantum_program.ChunkSpan` contains:

- ``start`` and ``stop``: The UTC timestamps marking the beginning and end of the chunk's
execution. Note that this span may include some amount of non-circuit time (for example,
scheduling overhead).
- ``parts``: A list of :class:`~.qiskit_ibm_runtime.quantum_program.ChunkPart` entries describing
which program items contributed to the chunk and how much of each item was included.

Each :class:`~.qiskit_ibm_runtime.quantum_program.ChunkPart` contains:

- ``idx_item``: The index of the program item (matching the order items were appended to the
program).
- ``size``: The number of configurations from that item that were included in this chunk.

.. code-block:: python

result = job.result()

for i, span in enumerate(result.metadata.chunk_timing):
duration = (span.stop - span.start).total_seconds()
print(f"Chunk {i}: {duration:.2f}s")
for part in span.parts:
print(f" Item {part.idx_item}: {part.size} configurations")

This is useful for understanding how the executor distributed work across shot loops, diagnosing
performance issues, and verifying that chunk sizes behaved as expected.
1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Guides

executor_basic
broadcasting
chunking