Repository navigation
Conversation
What the delta form and its hierarchy (#687, #688) buy, measured -- with the controls that make the measurements mean anything, which is most of what is here. Three independent places can run below backend precision, and they compose to within one iteration of the float64 baseline: the node-local solve specified by the accuracy it delivers, not by a format: SDC's fine solve needs about four digits, MLSDC's about six, MLSDC's coarse solve about two -- four orders of magnitude apart in the same run a whole coarse level float16, once the two O(1) cancellations are gone the fine level's state float16 early, climbing as the corrections shrink, float64 by the end Every claim has a control that must break: the stock hierarchy at the same reduced precision (which stalls), the finest level reduced (2.2e-05), the increment formed by subtraction (3.4e-06), an unscaled float16 correction (5.9e-05, which is float16's smallest normal). mlsdc.py reduced precision on a level, emulated -- for PETSc and DOLFIN, which fix their scalar type at build time, and the controls cascade.py storage precision raised as the iteration converges, on an indicator the delta form supplies itself: step up while |delta| < safety * eps * |u| paradiag.py ParaDiag needs no reformulation at all -- it is already iterative refinement over the whole block -- so this is two precision knobs and the tests that price them problems*.py the analytic increments and correction solves, on numpy, PETSc and FEniCS run_*.py one driver per backend, each printing and asserting its own matrix Registered in the project CI matrix and the docs index. The optional-backend tests live under pySDC/tests/test_projects/test_DeltaSDC, following test_step_7: that CI job selects by marker while the project job installs only this project's environment. 41 project tests and 4 optional-backend tests pass, on numpy 1.26.4 and 2.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eration asks for Three talk figures, and the two changes to allencahn_delta they needed. allencahn_delta could not run its node-local solve at float16: SciPy holds no half-precision sparse matrix, and the correction underflows float16's smallest normal (6.1e-5) after a handful of sweeps. Both are now addressed the way heat_delta already did it: * ``normalize`` scales the unknown to O(1) around the whole Newton loop. Unlike the linear case this is not a wrapper around the solve -- the increment expansion carries the scale on its quadratic and cubic terms, so it stays exact. * ``solve_precision`` now separates storage from arithmetic. The correction is stored at the requested format, operators and Krylov run at ``promote_types(solve_precision, float32)`` -- the convention the genuinely reduced-precision levels already use, and what half-precision hardware does anyway. Measured on Allen-Cahn, 16 iterations: fp64 floors at 9.6e-13, normalised fp16 at 7.5e-13 two iterations later, unnormalised fp16 stalls at 1.8e-06. The demo table is unchanged row for row. plot_mixed_precision.py draws that comparison for heat and Allen-Cahn. plot_delivered_accuracy.py sweeps the relative accuracy eta of the returned correction with the project's existing heat_inexact: 14 iterations from 1e-16 all the way to 3e-5, then 15/16/18 at 1e-4/1e-3/1e-2, with the residual floor never moving. A real float16 solve lands one iteration below the sweep at its own unit roundoff, the random perturbation being the more pessimistic model of the two. The generated figures are not committed; the scripts reproduce them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on GPUs Three things that could not run on a GPU and that no GPU test exercised: - `get_finite_difference_matrix` edits the boundary rows of a Dirichlet or Neumann operator in `lil` format, which CuPy does not have, so every non-periodic FD problem raised NotImplementedError with `useGPU=True`. The 1D stencil is now built on the host and only the finished matrix moves to the device, as `mesh_to_mesh` already does. - The four Allen-Cahn Newton solvers formed their residual on the datatype, whose `__abs__` is a norm. NumPy's `linalg.norm` strips the wrapper first, CuPy's does not, so every solve died on `cupy_mesh`. They now work on plain arrays. The GPU test compared only `eval_f`; it now compares the solvers too. - `run_mpi_tests.sh` dropped `PYTEST_SERIAL_EXTRA` for a tree that declares no MPI ranks, so such a tree ran its GPU tests on one device of the four the job rents. Measured on a narrowed run: 106 s on one worker, 56 s on four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#691 moved the Allen-Cahn problems to wells at 0 and 1: the reaction is now (1 / 2 eps^2) v (1 - v^2) in v = 2u - 1. The master merge applied cleanly, but allencahn_delta's analytic increment and Jacobian still expanded the old (1 / eps^2) u (1 - u^2), so the correction solve no longer solved the problem's own equation: six project tests failed, one answer off by 3.7e14, and the suite took 17 minutes instead of 13 seconds. Same expansion, written in v with d = 2 delta, so every term still carries a factor of the correction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project's problem classes now take `useGPU` like the rest of pySDC: allencahn_delta, heat_delta and heat_paradiag compute through xp/xsp/linalg, and round_value rounds any array with `astype`. `run_demo.py --gpu` reproduces both README tables on a T4, iteration for iteration (NumPy 2 counts). `test_gpu.py` (cupy marker, so the Modal GPU job picks it up) checks every row of both tables as its own test -- iteration count, distance to the fp64 peer, and that every control still fails -- so xdist spreads them over the four devices; plus the genuine-precision fingerprints and the ParaDiag complex64 path. New, and timed on hardware rather than emulated: - problems.heat_solve_dtype: a CG correction solve that genuinely runs in fp32. - run_gpu.py: time to solution of the ladder, 2D heat, CPU or GPU. At 1024^2 on a T4, fp32 CG plus an fp32 coarse level is 2.8x faster than fp64 SDC at the same answer; fp32 alone is 1.7x (the sparse bandwidth ratio). An H100 gains ~1.1x: there a CG iteration is launch overhead, not memory traffic. - ParaDiag on the GPU: with periodic boundaries the node-local solve is an FFT diagonalisation (exact), and the reduced transform is one matmul on either device. The README's ParaDiag table reproduces on a T4. At scale complex64 costs one iteration at small alpha; AdaptiveAlpha must be told the precision (inner_tol) or it drives alpha where complex64 diverges for an iteration. The README records all of it, and replaces "none of this is a statement about speed" with the measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acy spec with it Neither SciPy's nor CuPy's sparse matrices hold float16, so no node-local solve in the project could compute in half precision. heat_solve_dtype gains solver_type='FFT' for periodic grids: the operator is circulant, so the solve is a forward FFT, a division and an inverse FFT at solve_dtype. At float16 on a GPU those are cuFFT's complex32 transforms (cupy.cuda.cufft.XtPlanNd), with every value stored and every product rounded in half precision. On a CPU the same flag is an emulation, rounding through float16 around a complex64 transform. The rhs is normalised and the transforms unitary, which keeps an unnormalised 1024^2 transform (1e6) inside float16's 65504. The README's delivered-accuracy table, measured by spoiling exact solves, predicts what half precision (eta ~ 4e-3 at 1024^2) costs. Measured on a T4 with real arithmetic (run_gpu.py --fft): SDC +2, MLSDC coarse +1, MLSDC fine +4, against predicted +2..4, +1 and +4..9. Every row reaches its fp64 peer. The CPU emulation is optimistic: it finds the coarse solve free at 256^2, where the genuine arithmetic already costs an iteration. Tests: the FFT solve against a direct solve at all three precisions, with a lower bound so a solve that stayed in double fails (CPU); on the GPU, the half-precision fingerprint and the spec's ordering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master now lists projects through pySDC/projects/gallery.yml rather than a toctree in docs/source/index.rst, so the conflict there resolves to master's file, and DeltaSDC gets a gallery card instead. Its excerpt is the configuration table of run_gpu.py: what a precision ladder looks like in a description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… constant The serial and node-parallel rounding transfers had the same restrict and prolong overrides; they now come from one RoundedTransferMixin. ParaDiag's problem and heat_solve_dtype both diagonalised their periodic operator with the same few lines; that is now circulant_eigenvalues in problems.py. The PETSc problems import TOLERANCE_SAFETY instead of defining a second copy, and mlsdc.py no longer re-exports total_increment, which nothing imported from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and gallery plot_mixed_precision.py and plot_delivered_accuracy.py were talk figures that nothing ran. They now write into data/, and tests/test_plots.py runs both (about 7 s) and asserts what each figure shows: the normalised half-precision solve keeps the fp64 floor while the naive one stalls, and the iteration count is flat down to about five digits of solver accuracy. The CI uploads data/ with the project's artifacts, so the README embeds the figures where their claims are made, and the gallery card shows the heat equation one. matplotlib joins the project environment for it. The star in the delivered-accuracy figure was labelled a "real float16 solve"; it is heat_delta's emulation, and now says so. The README's file table gains run_gpu.py and the plot scripts, and a section underline that was one character short is fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acy table does test_heat_ladder demanded that a coarse solve rounded through float16 cost no iteration at all. It costs 7 on Apple's BLAS and 8 on Linux OpenBLAS, with the same NumPy (1.26 and 2.5) and SciPy on both, so the project job failed in CI and passed locally. That is the README's own delivered-accuracy table: an unrefined float16 solve delivers an eta between its 1e-4 and 1e-3 columns, 7 and 8 iterations, and the real half-precision FFT solve on a GPU already costs that one iteration at 256^2 and up. The test now allows it, and the README says "at most one extra iteration" instead of "for free". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
…ables on the CPU The coverage report had run_mpi.py and sweepers_MPI.py at 0%: the test spawned `mpirun python run_mpi.py` in a subprocess, which coverage does not follow. It now uses pySDC's convention for MPI tests, @pytest.mark.parallel(3), which etc/run_mpi_tests.sh runs under mpiexec in-process. run_mpi.main takes the precision as an argument instead of reading sys.argv, and a mismatch is an assertion every rank reaches, broadcast from rank 0, instead of comm.Abort. run_gpu.py was at 55%: nothing called its command line. A base test now runs its three tables (CG, FFT, ParaDiag with adaptive alpha) on a 16x16 grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
heat_paradiag replaced the problem's init and cast its operator to complex by hand, which predates `dtype` on the finite-difference problems: with dtype='complex128', now its default, the stock problem does both. What remains is what `dtype` cannot do -- solve at a different precision from the state, since dtype='complex64' would store the solution in single precision too, which is the control that stalls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pancetta
marked this pull request as draft
October 8, 2026 14:09
…rameters The node-local solve precision was `solve_precision` on five problem classes and `solve_dtype` on heat_solve_dtype. The library's own delta-form parameter is `correction_precision`, so the project now says `solve_precision` throughout, and heat_solve_dtype is folded into heat_delta: one heat problem, with `solver_type` choosing the route -- 'direct' the dense emulation it had, 'CG' and 'FFT' the genuine solves heat_solve_dtype had. The problems' own parameters (solve_precision, normalize, and the test helper's eta and seed) are registered with _makeAttributeAndRegister, as every pySDC problem parameter is, instead of being set as plain attributes that never reached prob.params. heat_paradiag uses the registered value rather than a private copy, and counts its solves as `jacobian_solves`, the library's name, instead of `paradiag_solve`. The README's demo tables come out identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion The README used ε for both the collocation residual (ε_m, ε_G = R ε_F, the update ε ← ε − δ + Δt QΔf) and a format's precision (ε|u|, ε/α), sometimes in the same paragraph. The residual is now r_m, as SDC is usually written, and ε means precision throughout, which the method section now says once. Where |r| meant the right-hand side of the correction solve, it now says so. The AdaptiveAlpha floor wrote its inner tolerance as τ, which is the FAS correction everywhere else in the README; it now names the parameter, inner_tol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 8, 2026
…lve contract #785 gave the finite-difference problems a solve precision of their own and an FFT solve, and #786 made the delta-form sweeper scale the linear route's right-hand side for any solver. The project's own versions of both go: - heat_delta keeps only what no library solver has, half precision on a sparse solver, which it emulates; every other precision is the library's genuine solve. Its CG and FFT routes and its `normalize` knob are gone. - heat_paradiag goes entirely: ParaDiag runs on the stock heat problem with dtype='complex128' and the library's solve_precision. paradiag.py keeps the transform at reduced precision, and circulant_eigenvalues goes with it. - run_gpu.py uses heatNd_unforced and heatNd_forced directly. - The project's FFT tests, which the library's test_FD_solve_precision now covers, and the GPU fingerprint of the FFT solve go. The heat ladder's "unnormalised fp16 solve" control no longer fails, because the sweeper now scales for that solver -- the contract working. It is removed from the table, and lives on in the library's test with the scaling switched off. The heat figure's control becomes stock SDC with the same half-precision solve (it stalls at 0.25); Allen-Cahn keeps its unscaled solver, since on the nonlinear route the solver scales itself. The README says so. Every iteration count in the heat table is unchanged; the fp32 rows' floors move within round-off (7.2e-14 to 8.8e-14), their solves now genuine rather than emulated. ParaDiag's table is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pancetta
marked this pull request as ready for review
October 9, 2026 05:34
…nd methods The delivered-accuracy specification -- what relative accuracy eta a node-local solve must deliver before it costs iterations -- was measured on 1D heat only. eta_spec.py measures it on 19 configurations: heat across four decades of stiffness, two to five nodes and three preconditioners, MLSDC with the fine or the coarse solve spoiled, advection from CFL 0.5 to 8, Allen-Cahn at three step sizes and Van der Pol from mu=1 to 100. Per configuration: the exact run's contraction rate rho, and eta*, the largest eta at which the run, and every run with a more accurate solve, costs at most one iteration more. inexact() generalises heat_inexact to any problem and both solve routes, so the sweep needs no problem twins; heat_inexact is now inexact(heat_delta). vanderpol_delta gives Van der Pol a correction solve -- an analytically expanded increment and a Newton iteration on the parent's solve_jacobian -- without which the delta sweeper would substitute the state. First result: eta* spans seven decades, 3e-8 to 1e-1, and does not follow the contraction rate measured on the solution. Where the solution's mode contracts much faster than the worst mode of the iteration (heat at small dt, advection), (rho_solution / rho_worst)^K predicts eta* to a factor of two: the solver's error excites every mode, and the slow ones outlast the converging solution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sweep showed the contraction rate measured on the solution does not predict eta*. Linear error propagation does: a sweep maps the collocation error by K = I - L^-1 R; a solve error made in sweep j enters as L^-1 D eps, is contracted by every later sweep and shows in the residual through R. Per mode of the operator that is an M x M computation, and with white noise of relative size eta on every solve -- what the spoiling does, and what rounding does -- the expected maximum residual it leaves at iteration K + 1 gives eta*. The only input from the exact run is the size of each node's correction per sweep; nonlinear problems use the Jacobian at the initial value, and MIN-SR-FLEX's changing preconditioner is followed sweep by sweep. Predicted against measured, for the 15 heat, advection and Allen-Cahn configurations: within a factor of 4 across seven decades, 3e-9 to 3e-3. Van der Pol is the exception, where the theory is optimistic by up to 300x: two unknowns are no white-noise statistics, and the linearisation is crude. IE is dropped, as agreed; MIN-SR-S is added for advection and Allen-Cahn, and MIN-SR-NS for non-stiff Van der Pol. MIN-SR-NS does not converge on stiff heat, and MIN-SR-FLEX, built for a fixed number of sweeps, takes 51 iterations to the tolerance there, so neither is in the heat set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The modal prediction needed the operator's spectrum, so a nonlinear problem had to be linearised at one state, and an opaque solver could not be treated at all. Perturbation theory needs neither. Two mechanisms, and eta* is the smaller: - noise: spoiling a single sweep j measures the residual its solve error leaves at iteration K + 1, a transfer coefficient T_j; independent errors superpose, so eta*_noise = tol / sqrt(sum T_j^2). K + 1 probing runs, nothing linearised. - rate: the error is relative to the correction and feeds back, so each sweep contracts by about rho + eta; an iteration is lost once (rho + eta)^(K+1) no longer beats rho^K: eta*_rate = rho (rho^(-1/(K+1)) - 1), from the exact run. Against the measured eta* of all 19 SDC configurations: within a factor of 5, from 3e-9 to 1e-1, Van der Pol included, which the noise mechanism alone mispredicted by up to 1000x -- it is the one case where the rate binds. eta*_rate is 1e-2 to 3e-2 everywhere, so for every format finer than bfloat16 only the noise mechanism matters. inexact() takes `calls`, the solve calls to spoil, which is how a single sweep is perturbed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
precision_theory.py predicts eta* without any inexact run: the exact run's per-sweep corrections, the test-equation matrices E(z), F(z) integrated over the operator's analytic spectral measure, Hoeffding + union bound, and the rate condition for the feedback. All 19 SDC configurations within the measurement's half-decade resolution or a factor 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iteration matrix K = I - P^-1 C acting on the error, the residual through C, restol instead of tol, eigenvalues instead of a spectral measure. Same numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…len-Cahn bound precision_theory: per-iteration contributions to Psi, bound(extra=m), and schedule(), a format per iteration from Theorem 2. Allen-Cahn uses the maximum over the interval of its frozen symmetric Jacobian's spectrum instead of the bulk-median heuristic; every prediction is now on the safe side. inexact(): a per-iteration schedule of noise levels or formats, rounding relative to the maximum (fp64, fp32, fp16, bf16). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…data genuine.py: node solves in true fp32/fp16/bf16 arithmetic (dense LU, every operation rounded to the format), the comparison table and time stepping with formats chosen from the previous step's run. precision_theory: a 'residual' error model for backward-stable solves (residual u ||D_j|| |delta_j|, transfer C K^n P^-1) next to the 'result' model, the rate condition on the forward error kappa u, analysis from any recorded run (record_run, analyse). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
work_precision.py minimises the work for a target error over M, dt, restol and the formats (fp64 versus formats chosen from the previous step, genuine low-precision solves) and draws the fronts. precision_theory.kappa: the a-priori stiffness of the node systems, for the first step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project keeps what computes; what presents results (the two plot scripts and their test, the figure and table printing of eta_spec, work_precision and genuine) goes, to come back as the reproduction scripts of a publication. work_precision.sweep returns the rows instead of writing pickles. The README now covers usage, contents and tests; the measurements, tables and lessons learned are no longer in it. References to the README in code and tests are reworded, and the cascade's rule that u[0] keeps its precision moved into its docstring. The gallery card has no plot until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nts after #808 run_demo's Allen-Cahn MLSDC transferred 64 -> 32 on a periodic grid without periodic=True. Before #808 that took the nested shortcut on the wrong grid (an O(h) interpolation error), since #808 the general non-periodic one. With the periodic transfer, 64 -> 32 is nested, and every MLSDC row of the table needs 16 sweeps instead of 20; the controls still stall at 60. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project now covers one question: whether the implicit solves of SDC can be done in reduced precision without losing the accuracy of the collocation problem. The broader study (emulated low precision on MLSDC's coarse levels, the storage precision ladder, the node-parallel variant, ParaDiag, PETSc, FEniCS and the GPU runs, with their tests) is removed; the code and the README with its results are kept in the tag deltasdc-precision-study-2026-11. eta_spec loses its MLSDC cases, and bf16 is dropped from the formats. The README is rewritten for what remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
pySDC/projects/DeltaSDC, the mixed-precision study. It is built on the delta-form sweepers and transfers already in master (#685, #687, #688, #690), on the finite-difference problems' own solve precision and FFT solve (#785), and on the correction-solve contract the delta-form sweeper now keeps (#786). It changes nothing in the library. Outside the project folder and its tests, the PR touches three files, six lines:ci_pipeline.yml: DeltaSDC in the project test matrix;docs/source/projects/DeltaSDC.rst: the page that includes the README;pySDC/projects/gallery.yml: the project's card on the website.The library fixes this needed on GPUs landed separately in #732, #733 and #734. What the project used to implement itself -- genuine reduced-precision solves, the FFT solve, ParaDiag's reduced-precision node-local solve, and the scaling a half-precision solver needs on the linear route -- now comes from #785 and #786. What stays here is the study: the emulation where no genuine route exists (PETSc, DOLFIN, half precision on a sparse solver), the precision cascade, the controls, the drivers, the figures and the README.
What the study shows
SDC in delta form solves for a correction rather than the state. So a node-local solve, or a whole coarse level, can run at reduced precision without capping the attainable accuracy: its error is relative to a quantity that goes to zero. The README has the details. The main results:
dtypeper level, fp32 CG solves, ParaDiag at complex64, and an fp16 FFT solve through cuFFT's complex32 transforms.run_gpu.py). At 1024² on a T4, fp32 solves plus an fp32 coarse level are 2.8× faster than fp64 SDC at the same answer, and fp32 alone gives 1.7×, the sparse bandwidth ratio. On an H100 the gain is about 1.1×, because a CG iteration there is launch overhead rather than memory traffic.The project's problem classes take
useGPUlike the rest of pySDC, andrun_demo.py --gpureproduces the README's tables on a GPU.Tests
base, project CI job): 47, includingtest_plots.py. It runs the two plot scripts (about 7 s), asserts what each figure shows, and writes them intodata/for the README and the gallery card.pySDC/tests/test_projects/test_DeltaSDC,fenics/petsc/mpi4py): 4, plus onebasecheck that the GPU tests cover every row of the README tables.cupy, picked up by the Modal job): 29 tests. They check every row of both README tables, one test per row so xdist spreads them over the four GPUs, plus fingerprints showing the reduced precision is genuine rather than silently upcast. 48 s on 4×T4 throughetc/modal_gpu_tests.py.Checked locally on the branch after merging current master: black and ruff over
pySDC, 50 CPU tests including FEniCS and PETSc, and the 2 MPI tests. The website build has not been run locally. The CI docs job builds it, and fails if an embedded figure is missing.For review
level_precisionand the other emulation knobs stay in the project on purpose: PETSc and DOLFIN fix their scalar type at build time, so for those backends emulation is the only way to measure anything.🤖 Generated with Claude Code