flopscope.
Understanding Flopscope

FLOP Counting Model

How Flopscope bills compute: the cost model, by family rule.

Cost model reference

Start here. This is the cost model's conceptual and audit reference. Read it to understand how billing works and to satisfy yourself that it is correct and non-gameable — you do not need to read every operation. The exhaustive, generated per-op list (every op with its cost_formula and weight) lives in ops.json and the website API pages; this doc explains the model by family rule so you can reason about a whole class at once.

flopscope bills compute as:

charged = int(flop_cost × weight)

How to read this

  1. Billing model & design principles — the one equation and why it is split into flop_cost and weight.
  2. Non-exploitability — the invariants that keep billing sound, and the test that enforces each.
  3. Cost by family — the rule + evidence + representative ops for the family you care about.
  4. Exhaustive per-op reference — drill into ops.json for one op's exact formula.

Completeness guarantee: every billed operation is classified in the registry and appears in ops.json with a cost_formula; tests/test_cost_model_coverage.py enforces both that, and that every op-class (ops.json area) is covered by a family rule below. So nothing billed is undocumented, even where this doc summarizes by rule.


Billing model & design principles

Every operation is charged charged = int(flop_cost × weight).

Two layers, on purpose. flop_cost is the operation count: every shape- and algorithm-dependent term lives here, so anyone — us or a participant — can read it off the formula and audit it function by function. weight is a separate per-element factor that captures how much more one element of an operation costs on real hardware: a plain add is 1, while a transcendental element (sin, exp, …) does many times more floating-point work. Rather than bill each op its exact measured ratio — noisy, machine-specific, and hard to audit — we group operations into a small fixed ladder of tiers ({0, 1, 8, 16}); that grouping is a deliberate competition-design choice. The rule that keeps the split honest — and the model non-gameable — is that a shape or algorithm constant never lives in a weight: anything depending on a matrix dimension or loop length belongs in flop_cost. (Enforced by tests/test_weight_tier_policy.py.)

We bill the textbook standard-algorithm cost, not literal BLAS/LAPACK. linalg.inv is billed 2n³ (the standard LU-based dgetrf+dgetri operation count) regardless of what the underlying library does; top-k SVD is billed as the standard truncated-algorithm cost. This keeps billing deterministic, hardware-independent, and composable.

The rest of this section defines the conventions these principles rest on.

FMA=2

Each floating-point multiply, add, subtract, divide, or square root counts as 1 FLOP. A fused multiply-add (FMA) therefore counts as 2. This matches the standard textbook convention. All formulas in this document are stated in FMA=2 units unless noted.

Comparison and select

A single comparison (>, ==, !=, …) or conditional-select (where, choose) counts as 1 FLOP. Sorting, partition, and percentile operations use this convention when counting per-element work.

Transcendental tier (weight 16.0)

Operations whose per-element cost is dominated by a libm minimax polynomial evaluation (sin, cos, tan, exp, log, arcsin, arccos, arctan, arcsinh, arccosh, arctanh, power, and their NumPy 2.x aliases) are billed at weight 16.0. The flop_cost formula is numel(output) (1 per element); the 16× factor is supplied entirely by the weight.

A subset of moderate-cost binary ops (floor_divide, mod/remainder, fmod, arctan2, hypot, logaddexp, logaddexp2) is grouped into the same tier (weight 16.0).

Half-tier transcendentals (weight 8.0)

Ops whose per-element work is a single cosine evaluation amortized over a cheap window formula (hamming, hanning) are billed at weight 8.0.

The unifying philosophy — compute, not logistics

flopscope meters computation on values, not data logistics. An operation is charged for the floating-point arithmetic and value-comparisons it performs to produce its output. It is free (weight 0) if it only relocates, replicates, selects-by-a-given-selector, or constant-fills values that already exist.

The decision procedure — apply these three steps in order to any op:

  1. View / metadata only (returns a view, inspects shape/dtype, no new buffer)? → Free (0).
  2. Does it produce output values by doing floating-point arithmetic, or by comparing element values?Charged. flop_cost = standard-algorithm op count; weight = hardware tier. This includes elementwise math, transcendentals, reductions, contraction (matmul/einsum), FFT, polynomial, random generation, and ops that derive a result by testing values: sort/argsort/partition/ searchsorted/unique*, nonzero/argwhere/flatnonzero/count_nonzero/ where(1-arg), clip/minimum/maximum, set-ops, and computed creators (arange/linspace/geomspace/logspace/vander).
  3. Otherwise it only relocates / replicates / selects-by-a-given-selector / constant-fills existing values → Free (0). This covers copy/concat/roll/ repeat/tile, gather/scatter & mask-select with a given selector, and constant init.

Key invariant: any predicate or index feeding a step-3 op was itself produced by a step-2 op and charged there. A free-tier op may never bundle value-arithmetic or value-comparison into its own cost.

After removing the gather tier, the only active weights are {0, 1, 8, 16}. Data-movement, selection-by-given-selector, and constant-init all carry weight 0. The only residual 4.0 entries are the submission-blocked callback ops (piecewise/apply_along_axis/apply_over_axes); they raise RemoteCallbackError on the grading backend and are left untouched.

Views and metadata (weight 0.0)

Weight 0 now covers four categories:

  1. Views / metadata — operations that return a view of existing memory or inspect metadata without touching element values: reshape, ravel, flatten, transpose, diagonal (as a view), squeeze, broadcast_to, astype (no copy), fftshift/ifftshift, linalg.diagonal, linalg.matrix_transpose, and all other shape/stride/dtype introspection ops.
  2. Copy / materialize — data-movement ops that copy or rearrange existing values into a new buffer: concatenate, stack, hstack, vstack, column_stack, dstack, tile, repeat, roll, tril, triu, copy, and kin.
  3. Gather / scatter & mask-select with a given selector — ops whose mask or index is an input: take, take_along_axis, put, put_along_axis, choose, where(cond, x, y) (3-arg), select, compress(mask, a), extract(mask, a), place, putmask.
  4. Constant init — ops that fill a new array with a fixed value (no per-element arithmetic): zeros, ones, empty, full, eye, identity, tri, zeros_like, ones_like, empty_like, full_like, meshgrid.

Refinement A — selection (resolves where/compress/extract/choose/select):

Selector given ⇒ free; selector derived by testing values ⇒ charged.

Free: where(cond, x, y), choose, select, compress(mask, a), extract(mask, a), take, take_along_axis. The mask/index is an input; any predicate that built it (e.g. a > 0.5greater) is a separate, separately-charged op.

Charged: where(cond) (1-arg, ≡ nonzero), nonzero, argwhere, flatnonzero, count_nonzero. These derive the selector by testing values (!= 0), so the test is their compute — they are charged numel(input) at weight 1.0.

Value-changing astype (to-bool !=0, float→int truncation, float-narrowing rounding) is also charged numel (weight 1.0) — a per-element value test. Lossless width casts (e.g. float32→float64) stay free. The method a.nonzero() is charged identically to fnp.nonzero(a).

Refinement B — creation (resolves init vs computed generators):

Constant-fill / replicate ⇒ free; compute-a-value-per-element ⇒ charged.

Free: zeros, ones, empty, full, eye, identity, tri, *_like, and meshgrid (pure replication of coordinate vectors — no per-element arithmetic).

Charged: arange, linspace (2×numel), geomspace, logspace (16×numel), vander (N(N-2)). If these were free a participant could synthesize an affine/ log-spaced ramp for free while the equivalent explicit x*step+start is charged — the substitution arbitrage the non-exploitability section forbids. Constant-fill has no such arithmetic equivalent, so it is free.

Composite ops (weight 1.0 with heterogeneous flop_cost)

When an operation mixes sub-tiers internally (e.g. random samplers, stats kernels, norms with SVD), all per-element factors are folded into flop_cost and the active weight is set to 1.0. This avoids double-counting with the tier factor.

NumPy 2.x ufunc aliases

NumPy 2.x introduced acos, acosh, asin, asinh, atan, atanh, atan2, pow, and divmod as canonical aliases for their arc* / power / floor_divide counterparts (identical ufunc objects). flopscope resolves these via _UFUNC_ALIAS_RENAMES in _weights.py so each alias charges the same weight as its canonical twin.


Non-exploitability

The cost model meters compute so a participant cannot do expensive real work while being billed cheaply. The two threats are under-count (an op billed below its true cost) and substitution arbitrage (routing the same work through a cheaper-billed but equivalent op). The model defends against both with invariants, each backed by a CI-enforced test you can open and read:

InvariantWhat it guaranteesEnforced by
Faithful costeach flop_cost is the real standard-algorithm op count, with every shape/algorithm constant inside flop_costper-op evidence in §Cost by family; test_cost_constant_unification.py, test_cost_formula_vs_code.py
Weight-tier policyevery active weight ∈ {0, 1, 8, 16}; arithmetic ops are 0 or 1; no algorithm constant in a weighttest_weight_tier_policy.py
No substitution arbitragea bit-identical alias cannot bill cheaper than its canonical (e.g. acos is arccos — the 16× ufunc-alias fix); equivalent contractions (dot/inner/matmul/einsum) share one cost enginetest_ufunc_alias_parity.py, test_random_weight_aliasing.py; the shared einsum engine (§Contraction)
No cheap in-op pathtop-k svd(k=) cannot yield a full decomposition below full price (the min(4mnk, economy) cap + k ≥ min → full guard); invalid k (< 1 or > min(m, n)) is rejected before any billingtest_svd_topk_cost.py (cap / guard / monotonicity); test_linalg.py (invalid-k ValueError)
Free-tier disciplineonly ops that perform no value arithmetic/comparison carry weight 0; a value-test is charged wherever it hides — including a.nonzero() (method), value-changing astype, where(1-arg), argwhere, flatnonzero, and count_nonzerotest_weight_tier_policy.py; test_data_movement_free_tier.py (free-labels consistency guard)
Memoization acceptedfree gather makes look-up-table reuse (precompute once with a charged op, then take for free) cheaper — this is deliberate: memoization is a legitimate optimization under a pure-compute metricdocumented here; test_data_movement_free_tier.py
End-to-end billingproduction flop_cost × weight is pinned per tier {0,1,8,16} (catches a silent weight regression)test_production_weight_billing.py

An auditor can read this table top-to-bottom and, for each claim, open the named test to see exactly what guarantees it. The first two rows are the load-bearing ones: an exact flop_cost defeats under-count, and the weight-tier policy (no constant in a weight) defeats the family of arbitrage exploits where a high-constant op is re-tiered cheaply.


Cost by family

Each family below is one rule + its evidence/citation + representative ops. The rule is the part to audit; the per-op tables are kept where each op carries a distinct cited constant (linalg, FFT, polynomial, stats, window, random) because those constants are the evidence — and because ops.json's generated cost_formula is coarse for many composite ops (it records per-operation where the real formula is shape-dependent). For families whose members all share one rule (copy/gather, views), only representatives are listed and the full set is a filter in ops.json.


Elementwise (pointwise unary and binary)

Family rule: flop_cost = numel(output).

Baseline tier (weight 1.0): arithmetic (+, −, ×, ÷, √), rounding (ceil, floor, trunc, rint, around/round), sign/abs, logical (not, and, or, xor), bitwise (and, or, xor, invert, left_shift, right_shift), comparisons (equal, not_equal, greater, less, greater_equal, less_equal), copies (positive, negative, real, imag, conj/conjugate, fabs, modf, frexp, spacing, nan_to_num, isclose, isneginf, isposinf, deg2rad/degrees, rad2deg/radians, ldexp, nextafter, copysign, heaviside, signbit), and their NumPy aliases.

Transcendental tier (weight 16.0): exp, exp2, expm1, log, log2, log10, log1p, cbrt, sin, cos, tan, sinh, cosh, tanh, arcsin, arccos, arctan, arcsinh, arccosh, arctanh, sinc, i0, power, angle, and their NumPy 2.x aliases (asin, acos, atan, asinh, acosh, atanh, atan2, pow).

Moderate binary tier (weight 16.0): arctan2/atan2, hypot, logaddexp, logaddexp2, floor_divide, mod/remainder, fmod, float_power.

Basis: DECLARED per-element FMA=2 convention and empirical calibration. Source: src/flopscope/_pointwise.py.


Reduction

Family rule: flop_cost = numel(input) − numel(output) (orbit-mapping model; one add or compare per element consumed by the reduction).

Ops that do more than one accumulation pass carry the extra passes in flop_cost (never in the weight column): the variance family makes four passes (mean-sum, centre, square, variance-sum), ptp makes two (max + min) plus the per-output subtract, and mean/average add the per-output divide.

Opflop_costweightbasis
sum, prod, max, min, any, all, nansum, nanmax, nanmin, nanprodnumel(input) − numel(output)1.0DECLARED reduction skeleton (one add or compare per consumed element)
cumsum, cumprod, nancumsum, nancumprod, cumulative_sum, cumulative_prodnumel(input) − num_output_slices (= n−1 for a full 1-D scan; product of non-reduced dims otherwise)1.0DECLARED: scan accumulation; output shape = input shape so the generic numel(in)−numel(out) formula evaluates to 0 — these use the correct per-slice count instead
mean, average (unweighted)numel(input)1.0DERIVED: reduction (numel−M) + M divides
average(weights=)3·numel − M, M = num output slices (1 for full reduction)1.0DERIVED: a·w multiply pass (numel) + a·w sum (numel−M) + weight sum (numel−M) + M divides
std, var, nanstd, nanvar≈ 4 × numel(input) (std: + M sqrt)1.0DERIVED four-pass: mean-sum, centre, square, var-sum (exact: 2·numel + 2·(numel−M) + 2M)
argmax, argminnumel(input) − num_output_slices (= n−1 for full 1-D; reduction_cost model)1.0DECLARED scan: same orbit model as reduction family
median, nanmedianaxis length per output slice1.0DECLARED; partition (introselect) per output
percentile, nanpercentile, quantile, nanquantileaxis length per output slice1.0DECLARED; partition (introselect) per output
ptp2 × numel(input) − numel(output)1.0DERIVED: max pass + min pass + M subtracts (2·(numel−M)+M)
count_nonzeronumel(input)1.0DECLARED comparison scan (every element tested regardless of axis)
nanmeannumel(input)1.0DERIVED: reduction (numel−M) + M divides; billed identically to mean

Source: src/flopscope/_pointwise.py; reduction accumulation model in src/flopscope/_accumulation/.


Contraction (einsum family)

Every op in this family is billed by one shared, symmetry-aware engine (_resolve_cost_and_output_symmetryeinsum_cost); the closed forms below are that engine's output specialised to each op's shapes, not separately maintained constants.

Family rule:

flop_cost = (2K − 1) × M
  • (2K − 1) is one length-K dot product: K multiplies + K − 1 adds (FMA=2).
  • K = product of the contracted (summed) axis dimensions.
  • M = number of output cells the engine computes. This is prod(output dims) for a generic contraction, but the engine reduces it to the unique-orbit count when it can prove the output is symmetric — when operands alias the same array (outer(v, v), inner(A, A)) or carry an as_symmetric tag. It never invents savings: A @ A for a general A still costs the full 2n³ − n², because A @ A is not symmetric.
OpContraction (k = contracted dim)flop_cost = (2K − 1) × M
matmul, linalg.matmul(m,k) · (k,n) → (m,n)2mkn − mn
dotmatrix (m,k)·(k,n) → (m,n); matrix–vector (m,k)·(k,) → (m,)2mkn − mn; m(2k − 1)
inner(m,k) · (n,k) → (m,n) — contracts the last axes2mkn − mn
tensordot, linalg.tensordotcontracts the chosen axes(2K − 1) × M
outer, linalg.outer(m,) · (n,) → (m,n) — nothing summed, K = 1mn
vdot, vecdot, linalg.vecdot(N,) · (N,) → scalarM = 12N − 1
matvec, vecmatmatrix·vector / vector·matrix, contracting k → length-mm(2k − 1)
kron(a,) ⊗ (b,) of flattened operands — nothing summed, K = 1a.size × b.size
einsumany subscriptswhole-expression accumulation (below)

Symmetry savings make M drop below prod(output) (here v is length n, A is n × n):

Expressiongeneric Msymmetric Mflop_cost
outer(v, v)n(n+1)/2n(n+1)/2
inner(A, A)n(n+1)/2(2n − 1) · n(n+1)/2

einsum runs the accumulation directly as (K − 1)·M + α, where α is the number of unique (output + contracted) index combinations — equal to K·M for a single clean contraction, but more general for multi-index or broadcast subscripts. A multi-operand einsum (≥ 3 operands) walks the opt_einsum optimal binary path and sums per-step costs. Batched/stacked variants of any row above multiply the closed form by the batch size.

Compound linalg ops are chains of matmuls, billed as the sum of their steps through the matmul_cost(m, k, n) helper — which itself delegates to einsum_cost('ij,jk->ik', …), so each step equals a 2-D matmul by construction (no duplicated 2mkn − mn constant to drift). linalg.pinv and linalg.lstsq build on the same helper.

Opflop_costbasis
linalg.matrix_power(⌊log₂ k⌋ + popcount(k) − 1) × matmul_cost(n, n, n)repeated squaring
linalg.multi_dotsum of optimal-chain matmul costs; each step 2mkn − mnoptimal chain order

All contraction ops use weight 1.0 — the shape formulas already carry the full FMA=2 cost. Source: _pointwise.py (op wrappers), _einsum.py (_resolve_cost_and_output_symmetry), _flops.py (einsum_cost, matmul_cost), _accumulation/ (accumulation model).


Generator (linspace, arange, and kin)

Opflop_costbasissource
arange2 × numel(output)DERIVED: start + i×step per element = 1 mul + 1 add (FMA=2)_array_ops.py; numpy arraytypes.c.src
linspace2 × numel(output) (handles broadcast start/stop and retstep=True)DERIVED: same affine model as arange_array_ops.py; commit 790d19af + retstep fix
geomspacenumel(output) (weight 16.0) → billed 16 × numel(output)DERIVED: flop_cost = numel(output); transcendental weight 16.0 (log + exp path)_array_ops.py
logspacenumel(output) (weight 16.0) → billed 16 × numel(output)DERIVED: same transcendental path as geomspace_array_ops.py
zeros, ones, full, zeros_like, ones_like, full_like, eye, identity, empty, empty_like, tri0 (allocation, no arithmetic)DECLARED free: constant-fill / replicate (Refinement B)_array_ops.py
meshgrid0 (free)DECLARED free: pure replication of coordinate vectors; no per-element arithmetic (Refinement B)_array_ops.py

Weight: 1.0 for arange and linspace; 16.0 for geomspace and logspace (transcendental path). Source: src/flopscope/_array_ops.py.


Sort and select

Family rule (DECLARED):

Opflop_costbasis
sort, argsortnum_slices × n × ⌈log₂ n⌉DECLARED comparison sort (n = axis length)
unique, unique_counts, unique_inverse, unique_values, unique_alln × ⌈log₂ n⌉ (axis=None); num_slices × shape[axis] × ⌈log₂ shape[axis]⌉ (axis=k)DECLARED sort-based; axis-aware per-slice
lexsortk × n × ⌈log₂ n⌉ (k = number of keys, n = sequence length)DECLARED
partition, argpartitionnum_slices × n × len(kth)DECLARED quickselect O(n) expected
searchsortedm × ⌈log₂ n⌉ (m = queries, n = sorted size)DECLARED binary search
sort_complexnum_slices × n × ⌈log₂ n⌉, n = a.shape[-1], num_slices = a.size // n (sorts last axis; equals flat formula only for 1-D)DECLARED
in1d, isin(n + m) × ⌈log₂(n + m)⌉ (sort path); max(sort_cost(n+m), 2nm) when numpy's masked-loop path triggers (small integer ar2)DECLARED algo-aware
intersect1dsort_cost(n) + sort_cost(m) + sort_cost(n+m) (default assume_unique=False); sort_cost(n+m) when assume_unique=TrueDECLARED: numpy calls unique() on both inputs when assume_unique is falsy
setdiff1d, setxor1d, union1d(n + m) × ⌈log₂(n + m)⌉DECLARED

All sort/select ops use weight 1.0; comparison = 1 FLOP convention. Source: src/flopscope/_sorting_ops.py, src/flopscope/_flops.py (sort_cost, search_cost).


Linalg direct (non-iterative)

All ops use weight 1.0 with all shape constants in flop_cost. Per-matrix cost is multiplied by the batch dimension product for stacked inputs. Zero-dim matrices charge 0.

Opflop_cost (per matrix)basissource
linalg.choleskyn³/3DERIVED: Cholesky factorization (dpotrf)_decompositions.py:cholesky_cost
linalg.qr (reduced/complete)2(2mnk − 2k³/3), k = min(m,n)DERIVED: factorization (dgeqrf) + Q-formation (dorgqr) ≈ same count_decompositions.py:qr_cost
linalg.qr (r/raw)2mnk − 2k³/3DERIVED: factorization only_decompositions.py:qr_cost
linalg.solve2n³/3 + 2n²×nrhsDERIVED: LU solve (dgesv = dgetrf + dgetrs)_solvers.py:solve_cost
linalg.inv2n³DERIVED: LU factorization + inversion (dgetrf + dgetri ≈ 2n³)_solvers.py:inv_cost
linalg.det2n³/3 + nDERIVED: LU factorization (dgetrf) + diagonal product_properties.py:det_cost
linalg.slogdet2n³/3 + 18nDERIVED: LU (dgetrf) + sum of log|diag| (abs + 16/elem log + reduce)_properties.py:slogdet_cost
linalg.norm (fro/L1/Linf)2 × numel(effective_shape) × n_groupsDERIVED: FMA=2 square+accumulate or abs+accumulate_properties.py:norm_cost
linalg.norm (ord=2, nuc)(2ab² + 2b³) × n_groups, a=max(m,n), b=min(m,n)DERIVED: values-only SVD cost per group_properties.py:norm_cost
linalg.vector_norm2 × numel(effective_shape) × n_groups (standard ord); (18 × numel + 16) × n_groups (general fractional p-norm: abs + pow per element)DERIVED: FMA=2_properties.py:vector_norm_cost
linalg.matrix_normsame as linalg.normDERIVED_properties.py
linalg.tracemin(m,n) × batchDERIVED: n−1 diagonal adds, batch-multiplied_properties.py:trace_cost
linalg.tensorinv2n³, n = prod(shape[:ind])DERIVED: via inv_solvers.py:tensorinv_cost
linalg.tensorsolve2n³/3 + 2n², n = prod(shape[ind:])DERIVED: via solve_solvers.py:tensorsolve_cost
linalg.matrix_rank2ab² + 2b³ + min(m,n), a=max(m,n), b=min(m,n)DERIVED: values-only SVD + min(m,n) threshold comparisons_properties.py:matrix_rank_cost
linalg.cond2ab² + 2b³ + 1 for ord∈{None,2,−2} (values-only SVD + 1 divide); 2k³ + 4mn + 1, k=min(m,n) for other ords (inv-based)DERIVED_properties.py:cond_cost
linalg.pinv6ab² + 20b³ + min(m,n) + n·min(m,n) + matmul\_cost(n, min(m,n), m), a=max(m,n), b=min(m,n)DERIVED: thin SVD (with vectors) + threshold + diagonal scale + reconstruction matmul_solvers.py:pinv_cost
linalg.lstsq6ab² + 20b³ + matmul\_cost(k,m,c) + k·c + matmul\_cost(n,k,c), k=min(m,n), c=#rhs colsDERIVED: thin SVD (with vectors) + U^T b + divide by s + reconstruction_solvers.py:lstsq_cost
linalg.cross3 × numel(output) (delegates to fnp.cross)DERIVED_aliases.py
linalg.multi_dotoptimal chain matmul cost; each step uses matmul_cost(m,k,n) = 2mkn − mnDERIVED_compound.py:multi_dot_cost
linalg.outer, linalg.tensordot, linalg.vecdot, linalg.matmul, linalg.matrix_powerdelegates to fnp.*DERIVED_compound.py, _aliases.py
linalg.diagonal, linalg.matrix_transpose0 (view)DECLARED free_aliases.py

Linalg iterative (eigen / SVD)

These ops use LAPACK drivers that iterate until convergence; counts are leading-order estimates of the standard operation count. All use weight 1.0.

Opflop_cost (per matrix)basissource
linalg.eig25n³DERIVED: dense eigendecomposition with eigenvectors — Hessenberg reduction + QR iteration + back-transform (dgeev)_decompositions.py:eig_cost
linalg.eigvals10n³DERIVED: dense eigenvalues only, no vectors (dgeev)_decompositions.py:eigvals_cost
linalg.eigh9n³DERIVED: symmetric tridiagonalization + divide-and-conquer with eigenvectors (dsyevd)_decompositions.py:eigh_cost
linalg.eigvalsh4n³/3DERIVED: symmetric tridiagonalization only, no vectors (dsyevd)_decompositions.py:eigvalsh_cost
linalg.svd (thin, full_matrices=False or square)6ab² + 20b³, a=max(m,n), b=min(m,n)DERIVED: thin SVD — Σ + U₁ + V (dgesdd thin path)_svd.py:svd_cost
linalg.svd (full, full_matrices=True and m≠n)4a²b + 22b³DERIVED: full SVD — forming the full m×m U dominates (dgesdd)_svd.py:svd_cost
linalg.svdvals2ab² + 2b³DERIVED: SVD values only, no vectors (dgesdd)_decompositions.py:svdvals_cost
roots10n³, n = stripped companion dimension (leading and trailing zero coefficients removed before companion matrix is built)DERIVED: companion-matrix eigvals (delegates to eigvals_cost on trimmed degree)_polynomial.py; consistent with polynomial-table roots row

Top-k (truncated) SVD

linalg.svd(..., k=) and linalg.svdvals(..., k=) accept a top-k parameter. For 1 ≤ k < min(m, n) the billed cost is

min(4·m·n·k, economy)

where economy is the full thin/values-only cost above. 4·m·n·k is the leading-order cost (FMA=2, Θ(mnk)) of a rank-k truncated SVD (two unavoidable passes over A). It is billed as the standard truncated-algorithm cost of the operation — consistent with how this model bills direct-linalg ops at their textbook standard-algorithm count rather than literal BLAS/LAPACK work — even though the reference implementation computes the full economy SVD and slices (results stay exact). Unlike the full case, values-only is not leading-order cheaper for top-k. k = min(m, n) (all components) bills the full economy cost, and the full_matrices full-U premium applies only to the full decomposition (k is None); so a complete decomposition can never be obtained below full price. Invalid k (< 1 or > min(m, n)) raises ValueError.

Accepted residual: because 4mnk < 6ab²+20b³ for all k ≤ min(m, n), the truncated rate applies up to k = min(m, n) − 1, so a caller can obtain up to min(m, n) − 1 exact singular vectors at the truncated rate. The guard ensures they can never obtain all min(m, n) components below full price.

Per-matrix cost is multiplied by the batch dimension product. Constants marked "provisional": iteration counts are input-dependent and the cubic constant is the standard textbook estimate.


FFT

Family rule (DERIVED, radix-2 FFT — 5 real ops per butterfly):

Opflop_costbasis
fft.fft, fft.ifft5 × N × ⌈log₂ N⌉, N = transform lengthDERIVED: 5 real ops per butterfly
fft.fft2, fft.ifft2, fft.fftn, fft.ifftn5 × N × Σᵢ⌈log₂ dᵢ⌉, N = prod(transform dims), dᵢ = individual axis lengthsDERIVED: sum of per-axis log₂ terms (coincides with 5N⌈log₂N⌉ only when all axes are the same power of 2)
fft.rfft, fft.irfft5 × (N/2) × ⌈log₂ N⌉DERIVED: real-input / real-output half-spectrum
fft.rfft2, fft.irfft2, fft.rfftn, fft.irfftn5 × (N/2) × Σᵢ⌈log₂ dᵢ⌉ (real half-spectrum)DERIVED: half-spectrum with per-axis log₂ sum
fft.hfft5 × (n_out/2) × ⌈log₂ n_out⌉DERIVED: hfft = irfft(conj(a)) — conjugate-symmetry halves the work
fft.ihfft5 × (n/2) × ⌈log₂ n⌉DERIVED: same hfft_cost(n) formula
fft.fftfreqn (index grid scaled by 1/(n*d) — one divide per output element)DECLARED: n divides
fft.rfftfreqn//2 + 1 (real-spectrum grid has n//2 + 1 elements)DECLARED: n//2 + 1 divides
fft.fftshift, fft.ifftshift0DECLARED free/metadata

All counted FFT ops use weight 1.0. Source: src/flopscope/numpy/fft/_transforms.py.


Polynomial

Opflop_costbasissource
polyval2 × deg × points (Horner: 1 mul + 1 add per coefficient per point, FMA=2)DERIVED_polynomial.py
polyfit2 × m × (deg+1)² (Vandermonde least-squares estimate)DERIVED: Vandermonde matrix construction + normal-equations cost; NOT an SVD path_polynomial.py
polyadd, polysubmax(len_a, len_b) (= max(n1, n2, 1))DERIVED: output length equals the longer polynomial_polynomial.py
polymul2nm − n − m (direct conv, FMA=2)DERIVED_polynomial.py
convolvefull: 2nm − n − m; valid: (2·min−1)·(max−min+1); same: exact dot-length sum per numpy C layoutDERIVED per-mode_pointwise.py:convolve
poly (1-D, build from roots)(3n² + n) // 2, n = len(roots) (iterative convolution with length-2 kernel per root; FMA=2)DERIVED_polynomial.py:poly_cost
polydert × n − t(t+1)/2, t = min(m, n−1) (order-aware; one multiply per surviving coefficient per derivative step)DERIVED_polynomial.py:polyder_cost
polyintm × n + m(m−1)/2 (order-aware; m passes each dividing n+j coefficients)DERIVED_polynomial.py:polyint_cost
roots10n³, n = stripped companion dimension (zero-leading/trailing coefficients stripped before companion matrix is built)DERIVED: delegates to eigvals_cost on trimmed degree_polynomial.py:roots_cost

Source: src/flopscope/_polynomial.py.


Random (module-level, Generator, RandomState)

Random ops are composite: the generation kernel cost and any setup cost (PRNG state update, rejection sampling) are folded into flop_cost; the weight tier varies by distribution family. Billed cost = flop_cost × weight.

Weight tiers:

  • weight 1.0 — uniform/integer/structural draws: rand, random, random_sample, ranf, sample, uniform, randint, integers, choice, shuffle, permutation, multivariate_normal.
  • weight 16.0 — transcendental samplers (every continuous/transformed distribution): normal, standard_normal, randn, exponential, standard_exponential, poisson, binomial, geometric, hypergeometric, negative_binomial, multinomial, beta, dirichlet, f, gamma, gumbel, laplace, logistic, lognormal, logseries, pareto, power, rayleigh, standard_cauchy, standard_gamma, standard_t, triangular, vonmises, wald, weibull, zipf, and all their Generator / RandomState counterparts.
Op / familyflop_costbasissource
random.rand, random.random, random.random_sample, random.ranf, random.samplenumel(output)DECLARED: 1 FLOP per uniform draw_cost_formulas.py
random.uniform3 × numel(output)DERIVED: affine map low + (high − low) × U = 1 sub + 1 mul + 1 add per element (FMA=2, three ops)_cost_formulas.py
random.randn, random.standard_normal, random.normalnumel(output) (weight 16.0) → billed 16 × numelDECLARED: flop_cost = numel(output); transcendental weight 16.0 from default_weights.json_cost_formulas.py
random.randint, random.integersnumel(output)DECLARED_cost_formulas.py
random.choice (replace=True, p=None)numel(output)DECLARED_cost_formulas.py
random.choice (replace=True, p≠None)numel(output) + 3n + m×⌈log₂ n⌉ (n=population, m=size)DERIVED: cumsum + normalize + searchsorted_cost_formulas.py
random.choice (replace=False, p=None)n (O(n) shuffle-based sampling: conservative ceiling on tail-shuffle)DECLARED_cost_formulas.py
random.choice (replace=False, p≠None)sort_cost(n) = n × ⌈log₂ n⌉ (data-dependent rejection loop with weights)DECLARED_cost_formulas.py
random.shuffle, random.permutationnumel(input)DECLARED: O(n) in-place shuffle_cost_formulas.py
random.exponentialnumel(output) (weight 16.0) → billed 16 × numelDECLARED: transcendental weight 16.0_cost_formulas.py
random.poisson, random.binomial, random.geometric, random.hypergeometric, random.negative_binomial, random.multinomialnumel(output) (weight 16.0) → billed 16 × numelDECLARED: transcendental weight 16.0_cost_formulas.py
random.multivariate_normal26d³ + 2Nd² + 16Nd (d=dims, N=size)DERIVED composite: SVD factorization of covariance (svd_cost(d,d,with_vectors=True) = 6d·d² + 20d³ = 26d³) + affine transform (2Nd²) + N·d transcendental normal draws (16Nd)_cost_formulas.py
random.beta, random.dirichlet, random.f, random.gamma, random.gumbel, random.laplace, random.logistic, random.lognormal, random.logseries, random.pareto, random.power, random.rayleigh, random.standard_cauchy, random.standard_exponential, random.standard_gamma, random.standard_t, random.triangular, random.vonmises, random.wald, random.weibull, random.zipfnumel(output) (weight 16.0) → 16 × numelDECLARED: flop_cost = numel(output); transcendental weight 16.0 for all continuous/transformed distributions_cost_formulas.py

Source: src/flopscope/numpy/random/_cost_formulas.py.


Stats

Stats ops are composite (weight 1.0; all per-element factors in flop_cost).

Opflop_cost (per element)basis
stats.norm.pdf27DERIVED: exp(17) + affine normalization(10); composite, weight 1.0
stats.norm.cdf48DERIVED: erf rational approx(45) + affine(3); composite, weight 1.0
stats.norm.ppf83DERIVED composite: degree-5 rational approximation + Newton step (erf + pdf + correction) + affine
stats.expon.pdf22DERIVED: z=(x−loc)/scale(2) + exp(−z)(17) + /scale(1) + where(2); weight 1.0
stats.expon.cdf22DERIVED: z(2) + exp(−z)(17) + 1−exp(1) + where(2); weight 1.0
stats.expon.ppf27DERIVED: loc−scale·log1p(−q)(19) + 3 where/cmp/and(8); weight 1.0
stats.cauchy.pdf6DERIVED pure-arithmetic: z=(x−loc)/scale; 1/(π·scale·(1+z²)) = 6 FLOPs/elem; weight 1.0
stats.cauchy.cdf20DERIVED: z(2) + arctan(16) + /π(1) + 0.5+(1); weight 1.0
stats.cauchy.ppf28DERIVED: q−0.5(1) + π·(1) + tan(16) + loc+scale·(2) + 3 where(8); weight 1.0
stats.logistic.pdf23DERIVED: z(2) + exp(−z)(17) + (1+ez)(1) + sq(1) + scale·(1) + div(1); weight 1.0
stats.logistic.cdf21DERIVED: z(2) + exp(−z)(17) + 1+ez(1) + 1/denom(1); weight 1.0
stats.logistic.ppf28DERIVED: 1−q(1) + q/(1−q)(1) + log(16) + loc+scale·(2) + 3 where(8); weight 1.0
stats.laplace.pdf22DERIVED: |x−loc|(3) + exp(−z)(17) + /(2·scale)(2); weight 1.0
stats.laplace.cdf40DERIVED composite: two eager exp branches + arithmetic/select; weight 1.0
stats.laplace.ppf51DERIVED composite: two eager log branches + edge selects; weight 1.0
stats.truncnorm.pdf28DERIVED composite: norm.pdf + cdf normalization; weight 1.0
stats.truncnorm.cdf51DERIVED composite: affine + norm.cdf + boundary selects; weight 1.0
stats.truncnorm.ppf81DERIVED composite: affine + rational + Newton with erf+exp; weight 1.0
stats.lognorm.pdf62DERIVED composite: log + exp + arithmetic per element; weight 1.0
stats.lognorm.cdf70DERIVED composite: log + erf rational approx + arithmetic; weight 1.0
stats.lognorm.ppf106DERIVED composite: ndtri + exp; weight 1.0
stats.uniform.pdf1DECLARED: 1 FLOP/elem
stats.uniform.cdf4DERIVED: sub + div + 2 clip compare/selects; weight 1.0

Source: src/flopscope/stats/.


Window

Opflop_costbasissource
bartlett4n (weight 1.0)DERIVED: compare + divide + add + select per sample (FMA=2, 4 ops/sample)_window.py:bartlett_cost
blackman40n (weight 1.0)DERIVED composite: 2 cosine evals at transcendental rate (16/elem each) + 8 mul/div/add per sample; all folded into flop_cost_window.py:blackman_cost
hamming2n (weight 8.0)DECLARED: cosine eval per sample at the half-transcendental tier_window.py:hamming_cost
hanning2n (weight 8.0)DECLARED: cosine eval per sample at the half-transcendental tier_window.py:hanning_cost
kaiser23n (weight 1.0)DERIVED composite: 1 Bessel I₀ eval at transcendental tier (16/elem) + 7 scalar FLOPs per sample; folded into flop_cost_window.py:kaiser_cost

Source: src/flopscope/_window.py.


Interp and histogram

Opflop_costbasissource
interp3m + m × ⌈log₂(numel(xp))⌉, m = numel(x) (interpolation arithmetic + binary search per query)DERIVED_counting_ops.py
histogram (integer bins)n × ⌈log₂(bins)⌉ (binary-search binning pass only)DERIVED_counting_ops.py
histogram (string bins, e.g. 'auto')n × (2 + estimator_cost + ⌈log₂ resolved_bins⌉) (deferred: resolved after the call; estimator costs: sturges/sqrt/rice=0, fd/auto=+1n, scott=+4n, doane=+6n, stone=+max(100,√n)n)DERIVED_counting_ops.py
histogram2d, histogramddsame as histogram per axisDERIVED_counting_ops.py
histogram_bin_edgesn (= max(n, 1)) for integer bins; string estimator bins: same formula as histogram string pathDECLARED: integer bins charge one comparison per element (no log₂ factor); estimator resolves bin count at call time_counting_ops.py
trapezoid, trapz4 × numel(y)DERIVED: (d·(y₁+y₂)/2).sum() ≈ 3 elementwise ops + sum-reduce per point, charged as a clean 4/point upper bound_pointwise.py; fixed in this branch

Source: src/flopscope/_counting_ops.py, src/flopscope/_array_ops.py.


Set ops

Opflop_costbasis
unique, unique_all, unique_counts, unique_inverse, unique_valuesn × ⌈log₂ n⌉DECLARED sort-based
in1d, isin(n+m) × ⌈log₂(n+m)⌉DECLARED sort-based
intersect1dsort_cost(n) + sort_cost(m) + sort_cost(n+m) (default); sort_cost(n+m) when assume_unique=TrueDECLARED: pre-sorts both inputs when assume_unique is falsy
setdiff1d, setxor1d, union1d(n+m) × ⌈log₂(n+m)⌉DECLARED sort-based
searchsortedm × ⌈log₂ n⌉DECLARED binary search

Comparison = 1 FLOP convention; weight 1.0.


Counting (diff, ediff1d, clip, allclose, isclose, count_nonzero, trace)

Opflop_costbasissource
clipmax(n_bounds, 1) × numel(output) (1 compare-select per bound; n_bounds=0,1,2; floor of 1 ensures materialising copy is not free)DERIVED_pointwise.py
count_nonzeronumel(input) (every element tested regardless of axis; comparison-scan model)DECLARED_pointwise.py
diffprod(a.shape[:ax]) × (n×L − n×(n+1)/2) × prod(a.shape[ax+1:]), L = a.shape[ax]DERIVED: n passes of L−k subtractions_pointwise.py
ediff1dary.size − 1 + size(to_begin) + size(to_end)DECLARED_pointwise.py
gradientbase: sum_ax 2·S·(L−2)/L; each coord-array axis adds a spacing surcharge (uniform: +3(L−1); non-uniform: +3S(L−2)/L + 10(L−2) + 3(L−1) + 4S/L)DERIVED_pointwise.py:gradient
allclose7·numel(broadcast) − 1 (6 FLOPs/elem tolerance core + numel−1 all-reduce)DERIVED_counting_ops.py
isclose6·numel(broadcast) (sub + 2·abs + mul + add + cmp per element)DECLARED_pointwise.py
trace (numpy.trace)min(ax1, ax2) × n_traces where n_traces = size / (shape[ax1] × shape[ax2]) (batch-multiplied)DERIVED_counting_ops.py:trace
correlatemode-aware: full = 2nm−n−m+1; valid = (2·min−1)·(max−min+1); same = exact dot-length sum per numpy C layoutDERIVED per-mode_pointwise.py:_correlate_cost
cross3 × numel(output) (2 muls + 1 sub per output scalar; 3-vec path preserves last dim, 2-D z-only drops last dim)DERIVED: FMA=2, 3 FLOPs per output element_pointwise.py:cross
cov2f²s + 2fs (f = features, s = samples)DERIVED: Gram term dot products of length s (2f²s) + centering pass fs elements × 2 FLOPs_pointwise.py:_cov_cost
corrcoef2f²s + 2fs + 2f² + fDERIVED: cov_cost + normalization (f² divides at weight 2.0 + f sqrts)_pointwise.py:_corrcoef_cost
unwrap11 × numel(input)DERIVED: 11 charged passes (diff, mod, cmp×2, bitwise, sub, abs, cmp, cumsum); 2 select passes (steps 8/12) are 3-arg where = free; prior value was 13_unwrap.py:unwrap_cost

Copy and gather

Family rule: free — pure relocation/selection.

Data-movement ops that copy, rearrange, or select-by-a-given-selector carry weight 0 and bill flop_cost = 0. They produce no per-element arithmetic and derive no selector by testing values — they only move existing values into a new buffer or layout. This covers: concatenate, stack, hstack, vstack, column_stack, dstack, block, bmat, tile, repeat, resize, roll, tril, triu, insert, append, delete, diag (both extract and construct), diagflat, fill_diagonal, take, take_along_axis, put, put_along_axis, choose, compress, extract, select, place, putmask, where(cond, x, y) (3-arg), unstack, and all other ops from the copy/materialize/gather/scatter families. (pad, copyto, and trim_zeros are not unconditionally free — see §Boundary ops.)

Selector-deriving siblings are charged (they test values to produce the selector):

Opflop_costbasis
nonzero, where(cond) (1-arg)numel(input) (weight 1.0)DECLARED: implicit != 0 scan per element
argwherenumel(input) (weight 1.0)DECLARED: ≡ transpose(nonzero(a))
flatnonzeronumel(input) (weight 1.0)DECLARED: ≡ nonzero(a.ravel())
count_nonzeronumel(input) (weight 1.0)DECLARED: comparison scan every element

These ops derive a selector by testing element values (!= 0), so the test is their compute cost. The predicate and the selection are the same step here — unlike the 3-arg where(cond, x, y) where the predicate (a separate charged op) is an input.

Worked examples:

ExpressionChargeReasoning
where(a > 0.5, x, y)pay greater = numel(a) for the predicate; the where (select) is freepredicate tests values (charged separately); selection by given mask is logistics
nonzero(a)charged numel(a)derives the selector by testing !=0 — value-test is its compute
arange(n)charged 2×numelcomputes start + i·step per element (1 mul + 1 add)
meshgrid(x, y)freereplicates x,y into grids; no per-element arithmetic
take(a, idx)freeindex given; pure gather
hstack([a, b])freecopies existing values into a new buffer
sort(a)charged n·⌈log₂ n⌉output order derived by comparing values
a.astype(float64)freewidth cast = representation only (no value change)
a.astype(bool)charged numel(a)per-element !=0 test = value-comparison

Source: src/flopscope/_array_ops.py.


Copy-and-gather: ops with distinct charged siblings

The table below lists ops whose cost formula differs from 0 because they contain value-arithmetic or perform I/O work beyond pure relocation:

Opflop_costbasissource
diag (extract, 2-D)0 (free — pure gather of diagonal elements)DECLARED: no arithmetic_array_ops.py
diag (construct, 1-D)0 (free — copy into diagonal of new matrix)DECLARED: no arithmetic_array_ops.py
diagonal0 (view)DECLARED: numpy.diagonal returns a read-only view_array_ops.py
copyto0 for same-dtype / where-mask copy / lossless widening; numel(dst) (or popcount(where)) for a value-changing (lossy) castDERIVED: path-aware — pure scatter-write and lossless width casts are free, a value-changing (lossy) cast is charged (see §Boundary ops)_array_ops.py
packbitsnumel(input) (weight 1.0)DECLARED: per-bit test+shift; value-test per element_array_ops.py
unpackbitsnumel(output) (weight 1.0)DECLARED: unpacks 8 bits per input byte; proportional to output_array_ops.py
mask_indices2n² + 8k (weight 1.0, k = selected pairs)DECLARED: n² mask scan (value test) + gather of 2k index values_array_ops.py

Boundary ops (free behavior + a value-computing path)

A free (weight-0) classification covers only an op's pure data-movement / structural behavior. Any parameter, mode, or path that computes or inspects values is charged with a reliable cost reusing the convention for that work; a path we cannot reliably bill is rejected with a clear error. These four ops carry weight 1.0 with a path-aware flop_cost:

Opfree path (flop_cost = 0)charged / rejected path
padconstant, edge, empty, wrap, reflect/symmetric (reflect_type='even')stat modes maximum/minimum/mean/median: Σᵢ stats_i·stat_len_i·cross_i (lanes from the input cross-section); linear_ramp and reflect_type='odd': 2·(numel_out − numel_in); mode=<callable> raises
ravel_multi_index2·(ndim − 1)·N (one unit stride), +N for mode='clip'/'wrap'
trim_zerosnumel(input) (value scan for the nonzero boundary)
copytosame-dtype copy, where-mask copy, lossless wideningnumel(dst) (or popcount(where)) for a value-changing (lossy) cast; lossless width casts are free

For pad stat modes: cross_i = numel_in // in_shape[i], stat_len_i = min(stat_length_i, in_shape[i]) (default = full axis), summed over padded axes only; a full-axis stat serves both sides (one reduction). mean adds one divide per stat output cell.


Functional / higher-order

Operations that apply a user-supplied callable across an array. flopscope bills the result the wrapper materializes (numpy runs the callback itself).

Submission caveat: these run a Python callback in-process and raise RemoteCallbackError on the client/server backend used for AIcrowd submissions, so they cannot appear in submitted code — their cost matters only for local runs.

Opflop_costsource
apply_along_axis, apply_over_axesnumel(output)_counting_ops.py
fromfunctionnumel(output)_array_ops.py
piecewisenumel(output) (the op bills its assembled result; each condition you pass in condlist is billed separately as its own comparison op)_counting_ops.py

View / free (weight 0.0)

Family rule: operations that return a view, re-interpret memory, or inspect metadata without touching element values charge 0 FLOPs.

Weight 0 now covers four sub-families (see §The unifying philosophy in the Billing model section for the full rule and both refinements):

  • Views / metadata: reshape, ravel, flatten, transpose, squeeze, expand_dims, broadcast_to, atleast_1d/2d/3d, asarray (no copy), asfortranarray, ascontiguousarray, astype (no copy / lossless-width), view, diagonal (view), moveaxis, swapaxes, ndim, shape, size, nbytes, itemsize, dtype, flags, base, data, ctypes, strides, T, linalg.diagonal, linalg.matrix_transpose, fft.fftshift, fft.ifftshift, isscalar, isfortran.
  • Copy / materialize: concatenate, stack, hstack, vstack, column_stack, dstack, block, bmat, tile, repeat, resize, roll, tril, triu, copy, insert, append, delete, diagflat, fill_diagonal, unstack, and kin.
  • Gather / scatter & mask-select (selector given): take, take_along_axis, put, put_along_axis, choose, where(cond, x, y) (3-arg), select, compress(mask, a), extract(mask, a), place, putmask.
  • Constant init: zeros, ones, empty, full, eye, identity, tri, zeros_like, ones_like, empty_like, full_like, meshgrid.

Source: src/flopscope/_array_ops.py.


Exhaustive per-op reference

The complete, per-op cost data lives in website/public/ops.json — one record per operation with name, module, area, category, weight, cost_formula, cost_formula_latex, notes, and summary. It is generated from the registry + weight tables by scripts/generate_api_docs.py and powers the website's API pages.

  • Find an op: filter ops.json by name, or browse the website API pages.
  • Filter a family: by area (core / fft / linalg / random / stats) or module.
  • It can't drift: CI runs scripts/generate_api_docs.py --check, which regenerates ops.json to a temp dir and fails if the committed file's cost-model fields differ (weight, cost_formula, category, notes, …). The summary field is sourced from the installed numpy's docstrings and is allowed to vary across the numpy-version matrix, so it is excluded from the check — which means the gate also proves the cost model is numpy-version-independent. Every billed op is present (aliases resolve transitively to their canonical), enforced by tests/test_cost_model_coverage.py.

Granularity note. ops.json is exhaustive in coverage — every op, with its weight and a formula string — but its cost_formula is coarse for many composite counted_custom ops, recording per-operation / varies where the real cost is shape-dependent. For those, the closed form and its derivation live in the family tables above. Treat ops.json as the complete index and this document as the precise reference; the completeness test ties them together.

On this page