Metadata-Version: 2.4
Name: hyperfold
Version: 0.1.0
Summary: NumPy-first Hyperfold Python API for the Hyperfold HPC library
Author: Lorenzo Fritzsch
License-Expression: LicenseRef-Hyperfold-Freeware
License-File: LICENSE.txt
License-File: THIRD_PARTY_NOTICES.txt
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: numpy>=1.26
Description-Content-Type: text/markdown

# hyperfold

`hyperfold` is a modern C++23 high-performance computing library for dense and
structured linear algebra. It provides solver pipelines and accelerator-aware
numerical kernels for CPU and CUDA backends, with an emphasis on numerical
correctness, predictable performance, and explicit resource ownership.

---

# Usage

## Hyperfold Python bindings

Hyperfold provides two mutually exclusive wheel variants with the same
`hyperfold` import name:

```bash
python -m pip install hyperfold
# Linux x86_64 with a compatible NVIDIA driver:
python -m pip install hyperfold-cuda
```

The CPU wheel requires NumPy 1.26 or newer. The CUDA wheel additionally installs
the tested CUDA 13.0 runtime (`nvidia-cuda-runtime==13.0.96`), cuBLAS
(`nvidia-cublas==13.1.1.3`) and cuSOLVER
(`nvidia-cusolver==12.0.4.66`) packages. CuPy and PyTorch are optional and
are never installed by Hyperfold. The two Hyperfold distributions must not be
installed together.

NumPy quickstart:

```python
import numpy as np
import hyperfold

a = np.array([[1, 2], [3, 4]], dtype=np.float32)
b = np.array([[5, 6], [7, 8]], dtype=np.float32)
system = np.array([[4, 1-1j], [1+1j, 3]], dtype=np.complex64)
rhs = np.array([1+2j, 3-1j], dtype=np.complex64)

product = hyperfold.matmul(a, b)
solution = hyperfold.solve(system, rhs)
```

CuPy and PyTorch use the same functions and preserve their framework through
zero-copy DLPack interchange when arrays are contiguous and on one CUDA device:

```python
import cupy as cp
import torch
import hyperfold
x_cp = cp.arange(8, dtype=cp.float32)
y_cp = hyperfold.add(x_cp, x_cp)
x_pt = torch.arange(8, dtype=torch.float32, device="cuda")
y_pt = hyperfold.scale(x_pt, 2.0)
assert isinstance(y_cp, cp.ndarray)
assert isinstance(y_pt, torch.Tensor)
```

Python sequences infer `float32` or `complex64`. Existing arrays retain their
dtype: common unsupported dtypes are rejected with the exact explicit
`astype(...)` or framework conversion needed, rather than being silently
rounded. Inputs are limited to vectors and matrices; v1 has no batching or
broadcasting.

| API group | `float32` | `complex64` | CPU | CUDA |
|---|---:|---:|---:|---:|
| `add`, `subtract`, `multiply`, `sum`, `scale` | yes | yes | yes | yes |
| `sqrt`, `conjugate`, `divide`, `inner`, `norm`, `vector_equal` | no | yes | yes | yes |
| `matmul` | yes | yes | yes | yes |
| other matrix operations and `eye` | no | yes | yes | yes |
| `solve`, eigensolvers, `partial_trace`, `is_unitary` | no | yes | yes | yes |

This table is derived from the currently registered CPU and CUDA backends. An
unsupported combination raises `hyperfold.BackendError`; Python does not
replace missing Hyperfold kernels with NumPy computations.

Reusable factorisations keep the solve operation on the factor itself:

```python
factor = hyperfold.lu_factor(system)
first = factor.solve(rhs)
second = factor.solve(2 * rhs)
```

`copy=None` borrows aligned C-contiguous storage and otherwise creates a fresh
same-device contiguous input. `copy=False` forbids those Python-boundary copies;
`copy=True` always normalises through fresh storage. Internal algorithmic
scratch and interleaved-complex to split-complex adaptation remain possible in
all three modes. `out=` must have the same framework, dtype, device and exact
shape, be writable and C-contiguous, and must not overlap an input.

```python
ctx = hyperfold.Context(cpu_threads=8, parallel_work_threshold=65536,
                        numa_policy="auto", pinning_policy="auto")
y = hyperfold.matmul(a, a, context=ctx)
cuda_ctx = hyperfold.Context(cuda_devices=0, stream_pool_size=32,
                             scheduling="auto", cross_device="reject")
```

CUDA calls pass Hyperfold's actual execution stream to each producer's
`__dlpack__` method, execute directly into framework-owned output memory, and
complete synchronously before returning. An external stream can be supplied as
`Context(external_stream=<CUDA stream pointer>)`; it remains caller-owned and
must outlive each call. A non-default external stream binds the context to the
first CUDA device on which it is used. Cross-device operands and implicit
host/device transfers are rejected. CUDA reductions return zero-dimensional
CuPy or PyTorch arrays; NumPy reductions return NumPy scalars.

Public wheels target CPython 3.11–3.14 (excluding PyPy and free-threaded builds):

- Linux x86_64 CPU wheels use an AVX2 baseline and fail at import with a clear
  diagnostic on older CPUs.
- macOS arm64 CPU wheels use NEON and Accelerate.
- Linux x86_64 CUDA wheels use CUDA 13 and include the AVX2 CPU backend; only a
  compatible system NVIDIA driver is required outside the virtual environment.

Source builds on macOS use Xcode's Apple Clang by default because Accelerate's
headers must match the active Apple SDK. Set `HYPERFOLD_MACOS_CXX` to an
alternative compatible C++ compiler path when an explicit override is needed.

Known v1 limitations are synchronous CUDA execution, non-negative signed 32-bit
`matrix_power` exponents, no JAX framework preservation, no futures, and no
`__array_ufunc__` or `__array_function__` integration. Compiled extensions need
one wheel for every supported Python/platform combination.

## Operation and kernel extensions

Python is the primary numerical API. Hyperfold deliberately provides three
extension paths with different distribution and dispatch contracts:

| Path | Intended use | Dispatch | Distribution |
|---|---|---|---|
| Python callbacks | Prototypes and orchestration | Exact Python signature match | Python source |
| Typed C++ registration | Source-integrated high-performance kernels | Native dispatch and adaptation | Rebuild the native host |
| C ABI plug-ins | Independently compiled CPU/CUDA kernel packages | Native dispatch and adaptation | Explicitly loaded shared library |

`HyperfoldContextBuilder` is the typed C++ registration boundary. Operation
identifiers use canonical dotted names and non-zero major versions
(`acme.copy@1`). Operand roles are library-defined: input index `N` is encoded
as `N`, while output index `N` is encoded as `0x80000000 | N`. Valid indices
are `0…0x7fffffff`. `Input0`–`Input2` and `Output0`–`Output2` are convenience
constants, `Output` aliases `Output0`, and `input_role(N)`/`output_role(N)`
support larger arities.

```cpp
using Input = Hyperfold::CpuDenseRealVectorArg<
    float, Hyperfold::OperandRole::Input0, Hyperfold::OperandAccess::Read>;
using Output = Hyperfold::CpuDenseRealVectorArg<
    float, Hyperfold::OperandRole::Output, Hyperfold::OperandAccess::Write>;

const Hyperfold::OperationId copy{"acme.copy", 1};
Hyperfold::HyperfoldContextBuilder builder;
auto acme = builder.extension("acme");
acme.register_operation({
    copy,
    {{Hyperfold::OperandRole::Input0, Hyperfold::OperandAccess::Read,
      Hyperfold::OperandKind::Vector},
     {Hyperfold::OperandRole::Output, Hyperfold::OperandAccess::Write,
      Hyperfold::OperandKind::Vector}}});
acme.register_cpu_kernel<Input, Output>(
    copy, Hyperfold::BackendKind::CpuScalar,
    [](Input::View input, Output::View output) {
      std::copy(input.data(), input.data() + input.size(), output.data());
    });

Hyperfold::HyperfoldContext context = std::move(builder).build();
```

The complete authoring, lifetime, concurrency, CUDA stream, packaging, and
ABI-compatibility contracts are documented in
[`docs/extensions.md`](docs/extensions.md). A buildable CPU plug-in with an
optional CUDA implementation is provided under [`examples/plugin`](examples/plugin).
Installed Python packages expose the two ABI headers through
`hyperfold.get_include()`; full C++ development artefacts remain part of the
native CMake installation rather than the wheel.

The operation contract is zero or more contiguous read-only inputs followed by
one or more contiguous write or read-write outputs. Every declared output is
mandatory and caller-owned. Python execution returns the caller-supplied object
for one output and an ordered tuple of those objects for multiple outputs.
Custom operand/backend kinds, automatic plug-in discovery, and automatic Python
output allocation are not supported.

Sealed Python contexts also provide deterministic operation introspection.
`Context.operations()` lists the available versioned operation IDs, while
`Context.describe()` reports each structural contract, its exact registered
backend/dtype/layout signatures, built-in or extension provenance, and optional
declarative shape and numerical metadata. The latter reports requirements,
direct algorithms, qualitative stability, asymptotic complexity, precision
behaviour, parameters, and explicit uncertainty without changing validation,
dispatch, or numerical execution.

Native wheels for the active Python interpreter can be built locally without a
container:

```bash
bash scripts/python/build_wheels.sh cpu wheelhouse
bash scripts/python/build_wheels.sh cuda wheelhouse
```

The CUDA build requires CUDA Toolkit 13.0 or newer. These native wheels are for
local testing and are not repaired portable distribution artefacts. Build the
CPython 3.11-3.14 distribution matrix with `matrix` mode, which requires
`cibuildwheel==4.1.0` and Docker on Linux:

```bash
bash scripts/python/build_wheels.sh cpu wheelhouse matrix
bash scripts/python/build_wheels.sh cuda wheelhouse matrix
```

Hyperfold is distributed under the proprietary [Hyperfold Freeware Licence](LICENSE.txt),
copyright Lorenzo Fritzsch. Both wheel variants include the licence and
[third-party notices](THIRD_PARTY_NOTICES.txt).
See [PyPI publishing](docs/pypi-publishing.md) for account setup, portable wheel
builds, validation, and uploads. CI does not build or publish wheel artefacts.

## CI workers

The macOS arm64 job dispatches a workflow in a private GitHub helper repository
after the containerised Linux C++ and Python tests pass. It fetches the exact
GitLab commit, builds and runs the C++ unit and integration tests, installs the
private Python package, and runs the Python tests. GitLab waits for that run,
downloads its diagnostic artefacts, and fails unless the run succeeds. The
macOS job uses Apple Silicon with NEON and Accelerate and is CPU-only. See
[the macOS bridge setup](docs/macos-ci.md) for the workflow, credentials,
timeouts, cancellation behaviour, and hosting limitations.

The Linux CUDA test job uses a fresh Scaleway GPU machine only after the macOS
job succeeds. Both jobs belong to the `platform-test` stage and require
`GITHUB_MACOS_REPOSITORY` to be configured. It pulls a versioned CUDA 13.0
build-and-test image from a private
Scaleway Container Registry namespace, mounts the checked-out commit into that
container, and then cleans up. The image already contains the CUDA compiler
and libraries, Compute Sanitizer, build tools, and Python test dependencies;
the fresh worker does not reinstall them. No wheel matrix, Docker manylinux
image, or wheel artefact is produced. Pipelines are created only for merge
requests. The paid GPU job runs automatically for same-project merge requests
and is not available to fork merge requests. The CPU jobs do not require
Scaleway credentials and are eligible to run for fork merge requests, subject
to hosted-runner availability in the source namespace. A newer commit
automatically cancels the replaceable jobs from an older pipeline on the same
merge-request branch. The GPU test job owns both provisioning and cleanup of
its Scaleway resources.

The Windows job remains disabled; its retained configuration requires GitLab's
`saas-windows-medium-amd64` hosted runner. The macOS bridge is enabled by setting
`GITHUB_MACOS_REPOSITORY` and runs only for same-project merge requests.

Enable **Auto-cancel redundant pipelines** under **Settings > CI/CD > General
Pipelines** in GitLab. The committed workflow then cancels every interruptible
job from an older pipeline when a newer commit is pushed to the same branch.

Configure these GitLab CI/CD variables:

- `SCW_ACCESS_KEY` and `SCW_SECRET_KEY`, masked and hidden, for a dedicated
  Scaleway IAM application;
- `SCW_ORGANIZATION_ID` and `SCW_PROJECT_ID`, masked and hidden, for a
  dedicated CI project;
- `SCW_SSH_PRIVATE_KEY` as a file variable containing a dedicated ED25519
  private key whose public key is registered with Scaleway. GitLab requires
  multiline SSH keys to use visible rather than masked visibility; the job
  receives the path to the temporary file and does not print its contents;
- `SCW_GPU_CI_IMAGE_REPOSITORY`, visible and non-secret, set to the image
  repository without a tag, for example
  `rg.fr-par.scw.cloud/example/hyperfold-ci`;
- `SCW_REGISTRY_PUSH_SECRET_KEY`, masked and hidden, for a dedicated IAM
  application with `ContainerRegistryFullAccess` on the CI project;
- `SCW_REGISTRY_PULL_SECRET_KEY`, masked and hidden, for a separate dedicated
  IAM application with `ContainerRegistryReadOnly` on the CI project.

Both registry variables contain only the relevant API secret key. Docker uses
the fixed username `nologin`; neither registry application's access-key ID is
required by the pipeline.

The image publisher uses rootless BuildKit and does not require privileged
Docker-in-Docker. The GitLab runner must allow the user namespaces and mount
operations required by rootless BuildKit. The image is tagged with a digest of
`docker/gpu-ci/`; its build runs automatically when that definition changes
and remains available as a manual merge-request job for recovery. The first
merge request that adds or changes the definition publishes the new image.
Pipelines for later merge requests that do not change it only pull that image;
additional pipelines in the image-changing merge request reuse the registry
build cache and the same content-derived tag.

Also configure `SCW_GPU_IMAGE` with the Marketplace label
`ubuntu_noble_gpu_os_13_nvidia`.
Marketplace catalogue UUIDs and zone-local image UUIDs are also accepted: the
job resolves and validates the configured value against the selected GPU type
and zone before creating an instance.
The committed defaults use `L4-1-24G` in `fr-par-1`, a 125 GB GPU root volume,
CUDA 13.0, compute capability 8.9, and Scaleway CLI 2.60.0. The Marketplace
image supplies the host-side NVIDIA driver and Docker environment; the private
container supplies the user-space CUDA toolchain. GPU types, zones, volume
settings, readiness timeouts, and architectures remain overridable through the
corresponding variables in `.gitlab-ci.yml`. Update `docker/gpu-ci/Dockerfile`
or its requirements file to change a containerised tool version.
`SCW_CLEANUP_ATTEMPTS` and `SCW_CLEANUP_RETRY_SECONDS` control the bounded
cleanup retry window and default to 30 attempts five seconds apart.

Scope the control-plane IAM application to the dedicated CI project with
`InstancesFullAccess`, `BlockStorageFullAccess`, and `SSHKeysReadOnly`. Block
Storage access is required to create and remove the GPU instance's SBS root
volume. Keep that project free of unrelated resources, cap its GPU quota, and
treat same-project contributors as trusted:
merge-request pipeline configuration can read even masked variables. The
remote source and test processes never receive the Scaleway control-plane or
registry credentials. The ephemeral host uses the read-only registry secret
through standard input, removes its temporary Docker configuration immediately
after the pull, and does not pass it into the test container.

GPU instances are force-deleted immediately with their IP and all attached
volumes. Every server, flexible IP, and tagged Block Storage volume carries
both `hyperfold-ci` and `hyperfold-ci-gpu`; cleanup requires both tags before it
reclaims a resource discovered outside the recorded state. The
`scaleway-gpu` resource group serialises GPU test jobs. The explicit
`bash scripts/ci/scaleway_ci.sh cleanup gpu --all-owned` recovery command can
reclaim tagged orphans; run it only when no GPU test job is active.

The job also records its server, flexible-IP, and SBS root-volume IDs. Its exit
trap makes an immediate cleanup attempt, probing exact IDs before deletion and
retaining any state whose absence cannot be confirmed. The same job repeats
the idempotent cleanup in GitLab `after_script`, including after script failure
or cancellation. `SCW_DEFER_CLEANUP_FAILURE=1` allows a successful GPU workload
to defer an unconfirmed trap cleanup to that final attempt, while
`AFTER_SCRIPT_IGNORE_ERRORS=false` makes an unconfirmed final cleanup fail the
owning job. Workload failures are never hidden. If the runner disappears before
either cleanup attempt completes, use the ownership-wide recovery command once
no GPU test job is active.

The CI image rebuilds its system CA bundle, logs the installed
`ca-certificates` version, and verifies TLS access to `api.scaleway.com` before
installing the Scaleway CLI. A GitLab Runner custom CA is added when supplied;
TLS verification is never disabled.

## Build the library
This project can be built using `make`:
```
make build
```

You'll find the compiled library in the `build/lib/` folder.

CUDA builds require CUDA Toolkit 13.0 or newer. By default, CUDA code is built
for Ampere, Ada, Hopper, and desktop/datacentre Blackwell architectures
(`80;86;89;90;100;103;120`). Override the target set when configuring a build:
```
cmake -S . -B build -DENABLE_CUDA=ON -DHYPERFOLD_CUDA_ARCHITECTURES="90;100;103"
```

## NUMA host memory
NUMA-aware host allocation is controlled by `HYPERFOLD_ENABLE_NUMA`, which accepts
`AUTO`, `ON`, or `OFF` and defaults to `AUTO`:
```
cmake -S . -B build -DHYPERFOLD_ENABLE_NUMA=ON
```

`AUTO` enables Linux `libnuma` support when the development files are available.
`ON` makes missing Linux `libnuma` support a configure-time error. `OFF` keeps
host allocation on ordinary aligned storage.

Host vectors, matrices, scalars, LU factors, and eigen outputs keep the requested
NUMA node separately from the effective placement reported by their topology
descriptor. `requested_numa_id()` is the caller's requested node;
`effective_numa_id()` and `numa_id()` report where placement was actually
honoured. If NUMA placement cannot be proven, the effective node is unknown
(`-1`) and `placement_guaranteed()` is false.

Topology descriptors now expose placement objects as the primary contract:
`host_placement()` returns requested/effective NUMA placement, and
`gpu_placement()` returns the CUDA device id plus any resolved GPU-local NUMA
node and PCI bus id. The node can be discovered from PCI topology or explicitly
configured. The older `numa_id()` and `gpu_id()` accessors remain
convenience accessors for the current effective host node and CUDA device id.

The default host allocation policy is fallback: an unavailable node or missing
`libnuma` falls back to aligned host storage while preserving the requested node
for diagnostics. Strict allocation is available through
`HostNumaAllocationPolicy::Strict` on low-level host and pinned-host allocation
paths; it fails instead of silently losing requested placement. CPU execution
routing uses effective host placement only, so fallback allocations with unknown
effective placement do not force a NUMA-local runtime.

CUDA host staging and cuSOLVER host workspaces use NUMA-aware pinned host
allocation when CUDA is enabled. Configure this through
`CudaHostMemoryOptions`, exposed by `CudaContext::Options::host_memory` and
`BackendExecutor::CudaOptions::host_memory`. `gpu_local_numa_node` defaults to
unknown. The `transfer_staging` and `solver_workspace` `HostMemoryOptions`
default to unknown NUMA placement and are resolved to the GPU-local NUMA node
when Linux PCI sysfs reports one; otherwise they remain unknown and use the
configured fallback or strict allocation policy.

For a multi-device `BackendExecutor`, `CudaOptions::host_memory` is the fallback
for every device. `CudaOptions::host_memory_by_device` can override it for each
CUDA device in the execution scope, including systems where PCI sysfs topology
is unavailable. Overrides for devices outside the execution scope are rejected.

## CUDA execution scope
CUDA execution supports an execution scope containing one or more CUDA devices.
`BackendExecutor::ExecutionScope::cuda_device(device_id)` creates a
single-device scope; `BackendExecutor::ExecutionScope::cuda_devices(...)`
creates a multi-device scope with duplicate ids removed while preserving the
first device as the primary fallback.

Each dispatched CUDA process selects one execution device. Scheduling is driven
by `GpuSchedulingOptions`: output-device scheduling is the default, first-input
scheduling can prefer the first GPU input, and preferred-device scheduling pins a
specific CUDA device. `BackendExecutor` validates the selected device and every
GPU operand against the execution scope before running process steps.

The default cross-device policy is `CrossDevicePolicy::Reject`, which requires
GPU operands to be colocated with the selected execution device. With
`PeerCopyIfAvailable`, CUDA uses peer copies when CUDA peer access is available
and rejects the operation otherwise. With `StagedHostCopy`, CUDA stages
cross-device transfers through pinned host memory using the configured CUDA
host staging NUMA policy.

`BackendExecutor::execute` completes process work through tail events recorded
on participating streams before returning. `BackendExecutor::submit` returns an
`Execution` token without adding that final wait; `wait()` provides the explicit
completion point, and destroying or replacing an unfinished token waits as part
of cleanup. Entity readiness events order later Hyperfold work without a
device-wide barrier, although native library operations may synchronise where
their contracts or status validation require it. A borrowed external CUDA
stream remains caller-owned and must outlive the execution token.

CUDA device resources derive their placement from the owning runtime. CUDA
memory pools, stream pools, runtime contexts, and frontend CUDA factories carry
the selected device placement into descriptors, including GPU-local NUMA metadata
when it is known.

## Full and Performance Testing
To build the tests, run:
```
make build-test
```

To build and run all the tests, run:
```
make test
```

To run benchmark smoke tests:
```
make test-perf
```

To run CPU-only coverage tests:
```
make test-covr-cpu
```

CUDA coverage and Compute Sanitizer checks are separate CUDA-machine workflows:
```
make test-covr-full
make test-cuda-sanitise
```

`test-covr-full` runs the complete test suite from a coverage-instrumented CUDA
build and writes reports under `build-covr-cuda/`. The gcovr report covers
host-side CUDA paths, including CUDA-enabled C++ code and launch/runtime paths;
it does not provide per-thread line coverage inside GPU kernels. The full
coverage target enables CUDA-language coverage by building C++ and NVCC host
code with the same compiler and by deriving the matching `gcov` from that
compiler. By default it probes NVCC's selected host compiler when `CXX` was not
explicitly set. Override `CUDA_COVERAGE_CXX` or standard `CUDAHOSTCXX` to choose
a supported host compiler. Use `test-cuda-sanitise` for device/runtime
correctness checks through NVIDIA Compute Sanitizer's `memcheck` tool. That
target registers one aggregate sanitiser test per CUDA-labelled test executable.
An aggregate is skipped when its required device test skips, including when no
CUDA device is visible; an empty GoogleTest selection and any real test or
sanitiser failure remain failures.

The test layer uses CTest labels, so focused runs are available after
`make build-test`:
```
ctest --test-dir build-test -L unit
ctest --test-dir build-test -L integration
ctest --test-dir build-test -L cuda
ctest --test-dir build-test -L numa-hardware
```

The `numa-hardware` tests are registered only on Linux builds with `libnuma`.
The policy test needs any allowed NUMA node, the page-residency test needs an
allowed nonzero node and permission to apply and query NUMA policy, and the
worker-affinity test needs an allowed nonzero node with at least three logical
CPUs. Missing topology skips only the affected test. A denied or unavailable
strict allocation or page-residency query also skips, while checks after a
successful strict allocation remain failures.

Benchmarks are built with `-DHYPERFOLD_BUILD_BENCHMARKS=ON`. `make test-perf` is
intentionally a short CTest smoke path. Use the benchmark targets below for
timing data:

```
make benchmark-local
make benchmark-full
make benchmark-full BENCHMARK_FILTER=Matmul
make benchmark-full BENCHMARK_FILTER='LuDecomp|Solve|Logdet|Eigen'
```

`benchmark-local` uses short repeated runs for quick feedback. `benchmark-full`
uses longer runs, 10 repetitions, aggregate reporting, tabular counters, and
JSON output under `benchmark-results/`. Both targets also write metadata JSON
with CPU, compiler, git revision, and CUDA/GPU details when available.

Individual Google Benchmark executables can also write machine-readable reports:
```
./build-perf/test/benchmark/backend_cpu/hyperfold_cpu_operation_benchmark --benchmark_filter=Matmul --benchmark_min_time=1s --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true --benchmark_out=cpu.json --benchmark_out_format=json
./build-perf/test/benchmark/backend_cuda/hyperfold_cuda_operation_benchmark --benchmark_filter=Matmul --benchmark_min_time=1s --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true --benchmark_out=cuda.json --benchmark_out_format=json
./build-perf/test/benchmark/backend_cpu/hyperfold_cpu_solver_benchmark --benchmark_filter='LuDecomp|Solve|Logdet|Eigen' --benchmark_min_time=1s --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true --benchmark_out=cpu_solver.json --benchmark_out_format=json
./build-perf/test/benchmark/backend_cuda/hyperfold_cuda_transfer_benchmark --benchmark_min_time=1s --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true --benchmark_out=cuda_transfer.json --benchmark_out_format=json
```

The operation benchmarks separate lowered process timing, dispatch-included
timing, direct materialiser-step timing, selected CUDA event-timed GPU regions,
and end-to-end synchronised CUDA latency. Real-time CUDA benchmark variants
explicitly wait for completion per iteration when using `BackendExecutor::submit`
or lower-level asynchronous paths; `BackendExecutor::execute` already waits
before returning. Counters include dimensions, throughput, bandwidth, and
GFLOP/s where the operation has a clear FLOP model.

Benchmark host placement can be controlled with environment variables:
```
HYPERFOLD_BENCHMARK_HOST_NUMA_NODE=1 make benchmark-local
HYPERFOLD_BENCHMARK_CUDA_TRANSFER_NUMA_NODE=1 make benchmark-full BENCHMARK_FILTER=CudaHostToDeviceTransfer
HYPERFOLD_BENCHMARK_CUDA_WORKSPACE_NUMA_NODE=1 make benchmark-full BENCHMARK_FILTER='LuDecomp|Solve|Logdet|Eigen'
HYPERFOLD_BENCHMARK_HOST_NUMA_POLICY=strict make benchmark-local
```

CPU benchmark host allocations default to requested NUMA node `0`. CUDA transfer
staging and solver workspace benchmark settings default to `auto`, which defers
node selection to CUDA runtime placement resolution. The values `auto` and
`unknown` both defer selection; an integer requests a concrete NUMA node. A
resolved request can still fall back to unknown effective placement when the
allocation policy permits it. Set `HYPERFOLD_BENCHMARK_HOST_NUMA_POLICY`,
`HYPERFOLD_BENCHMARK_CUDA_TRANSFER_NUMA_POLICY`, or
`HYPERFOLD_BENCHMARK_CUDA_WORKSPACE_NUMA_POLICY` to `fallback` or `strict` to
make the allocation policy explicit.

Benchmark metadata records raw NUMA environment requests, CPU affinity, online
NUMA nodes, the raw `CUDA_VISIBLE_DEVICES` environment value under
`visible_devices_env`, CUDA-visible GPU PCI metadata under `logical_gpus`, and
the physical GPU inventory when available. CUDA benchmark executables currently
use logical device `0`; its `logical_gpus` entry records the resulting physical
mapping when both CUDA and `nvidia-smi` expose enough information. The metadata
does not claim effective NUMA placement because only each completed allocation
can report whether its requested placement was honoured. Placement-sensitive
comparisons must use the corresponding `strict` policy so an unhonoured request
fails the benchmark instead of producing ambiguously placed measurements.

Benchmark JSON reports can be compared with:
```
make benchmark-compare BENCHMARK_BASELINE=benchmark-results/cpu_operation_full.json BENCHMARK_CANDIDATE=benchmark-results-next/cpu_operation_full.json
```

The `make build-perf`, `make test-perf`, and benchmark targets enable CUDA
`cudaProfilerStart`/`cudaProfilerStop` markers. CUDA benchmarks also use NVTX
ranges when the toolkit exposes NVTX, so Nsight can capture focused benchmark
regions:
```
make benchmark-profile BENCHMARK_FILTER=CudaComplexMatmul/Process
nsys profile --capture-range=cudaProfilerApi --trace=cuda,nvtx,osrt,cublas,cusolver ./build-perf/test/benchmark/backend_cuda/hyperfold_cuda_operation_benchmark --benchmark_filter=CudaComplexMatmul/Process --benchmark_min_time=0.1s --benchmark_repetitions=1
ncu --target-processes all ./build-perf/test/benchmark/backend_cuda/hyperfold_cuda_operation_benchmark --benchmark_filter=CudaComplexMatmulGpuTime --benchmark_min_time=0.1s --benchmark_repetitions=1
```
