Skip to content

feat(sampler): choose the sampling device with a device argument - #293

Merged
rafaelha merged 5 commits into
mainfrom
feat/device-option
Nov 12, 2026
Merged

rafaelha merged 5 commits into
mainfrom
feat/device-option

Conversation

@rafaelha

@rafaelha rafaelha commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

compile_sampler, compile_detector_sampler, CompiledMeasurementSampler and CompiledDetectorSampler take a device argument for strategy="active_statevector":

device Samples
"auto" (default) On the Apple GPU when it can run the circuit, else on the CPU, without a warning
"cpu" On the CPU, in float64
"metal" On the Apple GPU, or raises with the reason
sampler = circuit.compile_detector_sampler(device="metal")

"metal" raises where the GPU cannot run the circuit, at compile time and during a call:

  • RuntimeError: no Apple GPU, PyObjC's Metal not installed, or a process forked after its parent used the GPU
  • ValueError: a component wider than 11 active qubits, a rotation branch below float32's resolution (tape.RESOLVABLE), or noise beyond the GPU tables
  • MemoryError: out of GPU memory, at setup or during a call
  • runtime.CompileError: a Metal kernel that does not compile

Under "auto", every one of those falls back to the CPU silently; repr(sampler) and sampler.stats.device say where it samples. Before this PR, "auto" was the only behaviour and most fallbacks warned.

Other details:

  • TSIM_DEVICE=cpu still keeps "auto" on the CPU, so the benchmark harness and existing scripts are unchanged. An explicit device overrides it.
  • The stabilizer-rank strategies accept only "auto"; they run wherever JAX does.
  • A program without shot words has nothing to put on a GPU and stays on the CPU even with "metal".
  • A pickled "metal" sampler stays required when it is loaded.
  • device is a new keyword argument with an unchanged default, so this is a minor change under semantic versioning. "cuda" can be added when the CUDA backend (CUDA backend #219) lands.
  • Docs (install.mdx), the user skill and CHANGELOG.md are updated. The docs also say that with "auto" a seed reproduces samples only on the same kind of machine; use device="cpu" to reproduce across machines.

Skills

  • User skill (skills/simulating-circuits-with-tsim):
    • the three device values, and checking sampler.stats.device since "auto" is silent
    • when to use "metal" (long runs that must not drop to the CPU) and "cpu" (reproducibility, comparisons)
    • GPU calls of 10^6–10^7 shots
    • leave batch_size unset on the GPU: 2^20-shot launches that it can only lower
    • one process per GPU, and spawn rather than fork
    • GPU seeds depend on batch_size and on the kind of machine
    • the QEC reference notes that call sizing matters on the GPU too
  • benchmarking-tsim: the silent fallbacks (including tiny rotations), TSIM_DEVICE versus device=, and why GPU rates from small calls or a small batch_size measure overhead.
  • profiling-tsim: the same fallback and overhead notes.
  • testing-tsim-correctness: the seed invariant holds on the CPU only; Metal tests use device="metal" against a device="cpu" reference.

Test plan

  • uv run tsim-dev check: lint and 2776 tests passed on an M4 Pro, including the Metal integration tests.
  • New unit tests that run on any platform: device="cpu", unknown devices, a non-"auto" device with cat5, and "metal" where Metal is unavailable (raises), with "auto" falling back without warnings.
  • The Metal fallback tests (wide components, tiny rotations, oversized noise tables, setup and call-time out of memory, kernel compile failure, forked processes, pickling, TSIM_DEVICE) now check that "auto" is silent and "metal" raises.
  • test/conftest.py keeps the non-Metal tests on the CPU through the new runtime.unavailable().

Not run: just doc-build, and no benchmark. The sampling paths and kernels are unchanged; only the selection logic and the warnings changed.

🤖 Generated with Claude Code

compile_sampler, compile_detector_sampler and the sampler classes take
device="auto" (default), "cpu" or "metal". "auto" samples on an Apple GPU
when it can run the circuit and otherwise on the CPU, now without a
warning; "cpu" stays on the CPU; "metal" raises with the reason where the
GPU cannot run the circuit (no Apple GPU or PyObjC's Metal, forked process,
components wider than 11 active qubits, unresolvable rotations, noise
beyond the GPU tables, out of GPU memory, kernel compile errors), both at
compile time and when a call fails. TSIM_DEVICE=cpu keeps "auto" on the
CPU; an explicit device overrides it. The stabilizer-rank strategies take
only "auto".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T13:35:14.075167Z 9a64616 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The user skill explains device="auto"/"cpu"/"metal", checking
sampler.stats.device since "auto" no longer warns, why GPU calls need
10^6-10^7 shots and batch_size should stay unset (2^20-shot launches that
batch_size can only lower), one process per GPU and spawn over fork, and
that GPU seeds depend on batch_size and the machine. The benchmarking,
profiling and testing skills get the silent fallbacks, the call-size
caveat for GPU rates, and device="metal"/"cpu" in Metal tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a6461621e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +53
reason = runtime.unavailable()
if reason is not None:
if required:
raise RuntimeError(f"Cannot sample on Metal: {reason}.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check empty programs before requiring Metal

When device="metal" is used for a program with no shot words, such as Circuit("M 0").compile_detector_sampler(device="metal"), this availability check raises on a non-Mac before the num_words == 0 fast path is reached. Such a sampler has no work to place on a GPU and is explicitly intended to remain on the host either way, so the result currently varies by machine unnecessarily; check host.layout.num_words before requiring Metal availability.

Useful? React with 👍 / 👎.

Comment on lines +537 to 540
if runtime.forked_after_init():
if self._required:
raise RuntimeError(f"Cannot sample on Metal: {FORKED}.")
return on_cpu()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark permanently forked samplers as CPU-backed

For an automatic sampler created before a process forks, every call in the child takes this CPU path permanently, but _cpu_only remains false. Consequently sampler.stats.device and the repr continue to report metal even though all samples—including their seed stream and performance characteristics—come from the CPU, which is especially misleading now that this fallback is silent; mark the child sampler CPU-only before returning.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
6236 5199 83% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
src/tsim/circuit.py 95% 🟢
src/tsim/core/types.py 100% 🟢
src/tsim/sampler.py 95% 🟢
src/tsim/statevector/gpu/_init_.py 100% 🟢
src/tsim/statevector/metal/_init_.py 32% 🟢
src/tsim/statevector/metal/runtime.py 40% 🟢
src/tsim/statevector/metal/sampler.py 30% 🟢
TOTAL 70% 🟢

updated for commit: cb1c169 by action🐍

@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/tsim/sampler.py 87.50% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

rafaelha and others added 3 commits November 12, 2026 09:06
…rect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sampler compiled before a fork samples on the CPU in the child, but its
stats.device and repr still said "metal"; now that "auto" no longer warns,
they must say where it samples. Also correct the docs and skills: a GPU
batch is up to 2^20 shots, a GPU call's fixed cost is setting up launches
and waiting (Metal buffers are shared, nothing is copied back),
TSIM_DEVICE is read at compile time, and the rotation cutoff is about
6e-5 π.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rafaelha
rafaelha merged commit 7ec136e into main Nov 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant