diff --git a/docs/guides/chunking.rst b/docs/guides/chunking.rst new file mode 100644 index 0000000000..7340657d2f --- /dev/null +++ b/docs/guides/chunking.rst @@ -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. diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 35d5ef3f47..d18fd363ee 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -8,3 +8,4 @@ Guides executor_basic broadcasting + chunking