CPU, Metal and CUDA performance

Matched estimation problems on an Apple M4 and NVIDIA RTX 5070

Published

September 27, 2026

Trex’s tensor interface makes it possible to run many estimators on different devices, but does not guarantee acceleration. This study measures 14 workloads from 12 estimator classes, at three sizes each, using the same input files, precision and fitting settings on four backends. The Mac’s CPU and Metal GPU (mps) are distinct backends; the CUDA host’s CPU is included to separate within-machine acceleration from a comparison between computers.

These are measured fits, not extrapolated GPU timings. Each successful case has one discarded warm-up and three fresh timed fits. Numerical agreement is checked before presenting runtime ratios. Errors and iteration limits remain part of the results.

Main findings

CUDA pays off selectively as the problem grows; the M4 CPU is the best starting point for small fits in this suite. Every small workload is faster on its host CPU than on the tested GPU backend. At larger sizes the story depends strongly on the estimator.

  • Dense conditional moments and larger regressions benefit from CUDA. At the largest size, kernel MMR takes 28.8 ms on CUDA, versus 739 ms on the Intel CPU and 267 ms on the M4 CPU: 25.7× within-host acceleration, or 9.3× relative to the Mac. Large multinomial logit gains 12.0× versus the Intel CPU; two-way FE gains 8.4×.
  • Neural training benefits once the batches are large enough. The 50,000-row neural-sieve fit takes 54.5 ms on CUDA, 172 ms on Metal and 308 ms on the M4 CPU. This is the same 60-step training budget and agreeing predictions, not a comparison of times to a certified optimum.
  • Metal is not a general substitute for CPU here. It completes 21 of 42 configurations natively. Large MMR and neural-sieve fits are 1.54× and 1.79× faster than the M4 CPU; the tested likelihood, linear-sieve and SDID fits are slower. The other 21 configurations require unsupported operations or float64.
  • A GPU does not help every iterative estimator. The 128-state NFXP fit takes 263 ms on the M4 CPU and 805 ms on CUDA. SDID is also CPU-favored at these sizes, but every SDID run reaches its iteration cap, so its figures are timings of the same bounded computation—not times to convergence.

All 147 completed backend/problem combinations pass the predeclared agreement rule. This means computationally similar fitted outputs, not that all objectives have been optimized to arbitrary accuracy. The medium CUDA Poisson fit has a flagged score residual, and the neural fits intentionally stop after their fixed training budget.

Machines and software

Apple system CUDA system (fwk)
CPU Apple M4, 10 cores Intel Core i5-1240P, 12 cores / 16 threads
GPU Integrated M4 GPU, 10 cores; Metal/MPS NVIDIA GeForce RTX 5070, 12 GB
System memory 16 GiB unified 32 GB installed
OS macOS 26.6.2, arm64 Ubuntu Linux, x86-64
Torch 2.9.1 2.9.1+cu128
Python 3.12.11 3.12.12
CUDA runtime / driver — 12.8 / 595.84

Both environments use NumPy 2.4.2, SciPy 1.17.0, pandas 2.3.3 and pytorch-minimize 0.0.2. Eight intra-op/BLAS threads and one Torch inter-op thread are requested on each host. Neither CPU affinity nor clock rates are locked. Apple Accelerate and Linux BLAS/CUDA libraries still differ; matching package versions does not make the machines identical.

The machine metadata includes exact Python, kernel, Torch-build and source information. Both runs used the same Trex implementation and benchmark runner hashes. The benchmark source is pinned independently of subsequent documentation edits.

Workloads and sizes

The shape definitions matter: “large” is relative to each algorithm, not a common amount of computation. A dense kernel has quadratic storage; a linear regression with few columns does not.

Workload Small Medium Large Precision / fitting rule
Weighted least squares 1,000 × 8 20,000 × 32 200,000 × 64 float32; weighted solve + HC1
Two-way fixed effects 2,000 × 8 20,000 × 16 200,000 × 32 float64; weighted absorption + HC1
Binary logit 1,000 × 8 20,000 × 32 200,000 × 64 float32; LBFGS + model-based SE
Poisson 1,000 × 8 20,000 × 32 200,000 × 64 float32; LBFGS + model-based SE
Multinomial logit 1,000 × 6 × 4 20,000 × 12 × 5 200,000 × 24 × 6 float32; LBFGS + covariance
Two-step IV GMM 1,000 × 6 × 10 20,000 × 12 × 18 200,000 × 24 × 32 float64; Torch optimizer + sandwich SE
Kernel MMR 256 × 3 1,024 × 3 4,096 × 3 float32; dense RBF kernel, linear predictor
Linear sieve MD 1,000 × 6 20,000 × 6 200,000 × 6 float32; cubic instrument sieve, LBFGS
Neural sieve MD 1,000 × 6 10,000 × 6 50,000 × 6 float32; 64-unit network, 60 Adam steps
Matrix completion 64 × 40 256 × 120 1,024 × 240 float32; nuclear penalty, repeated SVD
Gaussian latent factors 64 × 40 256 × 120 1,024 × 240 float32; rank 3, 120 AdamW steps
Synthetic DID 100 × 50 500 × 120 2,000 × 240 float32; simplex Frank–Wolfe weights
NFXP 2,000 / 16 10,000 / 48 50,000 / 128 float64; observations / states, 2 actions
Hotz–Miller 2,000 / 16 10,000 / 48 50,000 / 128 float64; observations / states, 2 actions

Regression dimensions are n\times p; multinomial logit adds J choices and GMM adds q instruments. Panel dimensions are units \times periods. MMR and both sieve estimators use three instruments. All intercept columns are explicit. The TWFE panel has 20 periods and n/20 units; its known absorbed rank is supplied, so the experiment measures fitting rather than a CPU rank-counting prepass.

Common data and solver settings

All inputs are generated once with NumPy seeds 8270, 8271 and 8272 for the three sizes, saved as arrays, and copied to the other host. Every worker checks the input SHA-256 manifest. This avoids different CPU/CUDA random-number streams changing the estimation problem.

  • Regression errors are synthetic; WLS weights are positive lognormal functions of a covariate. The IV design adds a common shock to a regressor and its error. The conditional-moment examples similarly contain an endogenous regressor.
  • Likelihood fits start at zero, with maxiter=60, tol=1e-7. GMM uses two-step optimal weighting, maxiter=200, tol=1e-8. Linear MMR/sieve MD use LBFGS with 100 inner iterations, gradient tolerance 1e-6 and change tolerance 1e-9. MMR’s RBF bandwidth is 1; the sieve has degree 3 and ridge 1e-6.
  • Neural sieve MD uses copied initial weights, a 6–64–1 tanh network and 60 full-batch Adam steps at learning rate 0.01. This is fixed work, not a claim of optimization convergence.
  • Matrix completion and latent factors use rank-three synthetic signals and approximately 80% observed cells. Matrix completion uses lambda_fraction=0.1, maxiter=80, tol=1e-5. The latent model adds three covariates, penalty 0.001 and 120 AdamW steps at learning rate 0.02; its initialization includes a least-squares solve and SVD. Fitted predictions, not nonidentified factor signs, are compared.
  • SDID uses 90% control units, 75% pretreatment periods, a post-treatment shift of 1, and maxiter=1000, min_decrease=1e-4. Its stopping test squares min_decrease; the numerical-diagnostics section identifies cap hits.
  • Dynamic choice uses the same two-action replacement model, known transitions, discount factor 0.9 and copied initial parameters. It fits simulated state/choice observations with maxiter=40, tol=1e-7. This is not transition estimation or a long-panel dependence experiment.

End-to-end fit times

Times below are milliseconds: median [minimum, maximum] of three fits, after warm-up. Model construction, movement from host RAM, the public fit method, and copying fitted results back to CPU are included. Imports, disk I/O, data generation, SSH transfer, and post-fit quality diagnostics are excluded. Where the public fit computes standard errors, their cost is included. On narrow screens, scroll the tables horizontally to see all four backends.

† indicates a score diagnostic or iteration-cap warning; ‡ means the numerical agreement rule failed. The timing is still reported, but it is not presented as a speedup to an equivalent solution. Neural and latent-factor rows are fixed-work comparisons even when they agree closely.

Workload / dimensions M4 CPU M4 Metal Intel CPU RTX 5070 CUDA
Weighted least squares
1000 × 8
0.52
[0.51, 0.58]
unsupported 1.06
[1.05, 1.10]
1.78
[1.76, 1.78]
Two-way fixed effects
2000 × 8
1.83
[1.76, 1.86]
unsupported 2.37
[2.37, 2.79]
4.58
[4.53, 4.63]
Binary logit
1000 × 8
3.23
[3.19, 3.25]
58.84
[57.96, 59.43]
5.43
[5.42, 5.62]
16.64
[16.02, 17.00]
Poisson
1000 × 8
1.93
[1.92, 1.99]
55.98
[55.59, 56.06]
5.23
[5.21, 5.25]
16.87
[16.18, 16.87]
Multinomial logit
1000 × 6 × 4
21.58
[21.44, 21.69]
277.11
[276.39, 277.63]
31.60
[31.58, 31.68]
89.03
[86.10, 90.73]
Two-step IV GMM
1000 × 6 × 10
3.92
[3.91, 4.20]
unsupported 10.45
[10.42, 10.46]
21.62
[21.29, 21.87]
Kernel MMR
256 × 3
4.54
[4.40, 4.57]
38.64
[37.15, 39.53]
6.24
[6.20, 6.35]
12.46
[12.15, 12.95]
Linear sieve MD
1000 × 6
2.02
[2.01, 2.12]
57.28
[57.03, 59.21]
5.42
[5.33, 5.43]
15.40
[15.28, 15.59]
Neural sieve MD (fixed work)
1000 × 6
19.47
[19.40, 19.74]
57.86
[57.49, 60.53]
30.22
[30.09, 30.29]
53.20
[52.31, 54.26]
Matrix completion
64 × 40
3.14
[3.04, 3.21]
unsupported 7.28
[7.24, 7.34]
19.14
[19.06, 19.22]
Gaussian latent factors (fixed work)
64 × 40
28.14
[27.85, 28.39]
unsupported 58.45
[57.84, 59.55]
109.71
[87.36, 110.68]
Synthetic DID
100 × 50
71.18 †
[71.18, 71.42]
3430.73 †
[3418.93, 3437.52]
130.03 †
[129.43, 132.22]
499.37 †
[495.04, 503.25]
NFXP
2000 × 16
156.78
[156.09, 157.54]
unsupported 366.21
[365.98, 371.50]
675.34
[673.85, 680.45]
Hotz–Miller
2000 × 16
9.79
[9.01, 9.81]
unsupported 19.51
[19.46, 19.73]
48.99
[48.70, 49.54]
Workload / dimensions M4 CPU M4 Metal Intel CPU RTX 5070 CUDA
Weighted least squares
20000 × 32
7.87
[7.83, 8.02]
unsupported 6.65
[6.60, 6.80]
3.89
[3.88, 3.89]
Two-way fixed effects
20000 × 16
15.55
[15.48, 15.67]
unsupported 13.22
[13.18, 14.54]
6.62
[6.61, 7.04]
Binary logit
20000 × 32
20.25
[20.18, 20.29]
71.97
[71.63, 80.76]
72.25
[71.93, 126.92]
95.62
[93.22, 98.32]
Poisson
20000 × 32
5.84
[5.66, 5.90]
66.89
[66.08, 71.18]
8.39
[8.06, 8.48]
60.75 †
[60.62, 60.81]
Multinomial logit
20000 × 12 × 5
59.94
[58.10, 60.27]
433.67
[426.72, 442.39]
101.74
[101.55, 103.39]
77.16
[76.92, 85.24]
Two-step IV GMM
20000 × 12 × 18
19.16
[19.05, 19.17]
unsupported 44.05
[44.01, 44.84]
24.86
[24.68, 26.36]
Kernel MMR
1024 × 3
16.92
[16.88, 17.69]
51.16
[51.10, 51.19]
40.90
[40.73, 40.94]
13.97
[13.88, 14.13]
Linear sieve MD
20000 × 6
4.55
[4.34, 4.66]
46.53
[46.19, 46.74]
8.16
[8.07, 8.72]
13.03
[12.13, 13.75]
Neural sieve MD (fixed work)
10000 × 6
70.03
[69.79, 70.24]
62.14
[61.08, 63.23]
145.27
[141.82, 146.03]
53.13
[52.77, 53.53]
Matrix completion
256 × 120
14.18
[14.16, 14.27]
unsupported 25.44
[25.43, 25.44]
26.39
[26.36, 26.54]
Gaussian latent factors (fixed work)
256 × 120
99.56
[99.34, 99.64]
unsupported 170.13
[169.45, 170.91]
112.64
[112.24, 123.37]
Synthetic DID
500 × 120
96.53 †
[96.23, 96.71]
3433.54 †
[3429.31, 3444.50]
180.68 †
[180.22, 182.06]
494.00 †
[493.88, 494.84]
NFXP
10000 × 48
218.50
[217.44, 219.51]
unsupported 413.24
[410.65, 413.39]
739.81
[739.40, 744.98]
Hotz–Miller
10000 × 48
13.13
[13.07, 13.29]
unsupported 26.88
[26.84, 26.90]
49.01
[48.53, 49.52]
Workload / dimensions M4 CPU M4 Metal Intel CPU RTX 5070 CUDA
Weighted least squares
200000 × 64
184.48
[184.08, 189.50]
unsupported 224.94
[214.43, 227.36]
35.91
[35.82, 35.91]
Two-way fixed effects
200000 × 32
296.81
[291.78, 302.87]
unsupported 568.33
[564.32, 585.44]
67.49
[67.47, 67.50]
Binary logit
200000 × 64
60.88
[60.87, 61.14]
238.84
[238.25, 239.01]
294.73
[286.91, 327.71]
37.01
[36.96, 50.18]
Poisson
200000 × 64
49.63
[48.92, 49.76]
97.67
[93.95, 98.80]
102.09
[101.04, 102.60]
48.61
[46.24, 50.37]
Multinomial logit
200000 × 24 × 6
253.56
[246.55, 282.56]
282.51
[282.22, 286.79]
711.78
[708.72, 716.36]
59.25
[58.91, 61.11]
Two-step IV GMM
200000 × 24 × 32
348.59
[347.96, 353.16]
unsupported 1052.00
[1039.77, 1061.22]
165.94
[164.94, 166.21]
Kernel MMR
4096 × 3
266.95
[266.57, 272.42]
173.83
[170.18, 174.90]
739.26
[730.62, 755.48]
28.80
[28.67, 28.81]
Linear sieve MD
200000 × 6
27.23
[27.03, 27.31]
63.36
[63.17, 64.19]
38.81
[38.57, 38.88]
16.20
[16.14, 16.60]
Neural sieve MD (fixed work)
50000 × 6
307.72
[307.13, 310.62]
171.84
[171.08, 172.31]
647.81
[646.55, 655.59]
54.50
[53.16, 55.10]
Matrix completion
1024 × 240
76.80
[76.07, 77.42]
unsupported 72.84
[71.86, 75.78]
44.63
[44.46, 44.81]
Gaussian latent factors (fixed work)
1024 × 240
455.03
[451.97, 456.28]
unsupported 814.80
[792.84, 864.59]
126.16
[125.06, 129.76]
Synthetic DID
2000 × 240
129.35 †
[126.48, 129.37]
3516.95 †
[3514.49, 3560.99]
240.95 †
[240.95, 243.84]
501.46 †
[500.40, 504.35]
NFXP
50000 × 128
262.86
[262.20, 263.39]
unsupported 559.09
[558.59, 567.69]
805.19
[802.97, 814.91]
Hotz–Miller
50000 × 128
33.39
[32.94, 33.42]
unsupported 67.57
[67.14, 67.80]
63.45
[62.36, 63.71]

Within-machine acceleration

The ratio is CPU median time divided by GPU median time on the same host. A value above 1 favors the GPU; below 1 favors that machine’s CPU. An M4 CPU versus RTX 5070 ratio answers a different question because it also changes the host, memory system and software backend. Those cross-machine ratios are included separately in the downloadable CSV.

(a) Transfer-inclusive CPU/GPU runtime ratios on each host. Gray cells did not run natively on that backend. An asterisk marks a fixed-step workload; a dagger marks an iteration-cap or score warning. Colors use a common logarithmic scale, clipped at the endpoints; labels give the actual ratios.
(b)
Figure 1

Numerical agreement and stopping

Speed is only useful if the same estimation problem is being solved. The agreement rule was committed before the repeated timing run. For each device, every measured repetition is compared with every Mac CPU repetition on that exact case. For a target vector v, define

e_v=\max_{r,s}\frac{\|v_{\mathrm{device},r}-v_{\mathrm{M4\ CPU},s}\|_\infty} {1+\|v_{\mathrm{M4\ CPU},s}\|_\infty}.

The bound is 10^{-3} for float32 and 10^{-5} for float64. Coefficients and standard errors are checked where available; prediction grids are checked for neural, latent-factor and matrix-completion fits; SDID compares the ATT rather than requiring unique donor weights. Losses, weighted residual RMSE, or both SDID weight objectives must additionally agree within 10^{-4} under the same scaling. The denominator’s 1 makes this an absolute-plus-relative criterion, not a percentage error when the reference is near zero.

Backend Fits completed Agreement passes Worst target error Worst criterion error
M4 CPU 42 / 42 42 / 42 1.60e-07 1.83e-09
M4 Metal 21 / 42 21 / 21 1.35e-04 1.75e-07
Intel CPU 42 / 42 42 / 42 4.00e-04 1.09e-07
RTX 5070 CUDA 42 / 42 42 / 42 2.04e-04 1.02e-06

Agreement is not an optimality certificate. The suite also records normalized score infinity-norms for likelihood, GMM and conditional-moment objectives, plus the iterations exposed by each public estimator. A score above 10^{-4} is flagged separately. Score scales differ across objectives, so this is a diagnostic threshold, not a universal statistical accuracy guarantee.

Workload Size Backend Diagnostic
Poisson medium RTX 5070 CUDA score 1.89e-04
Neural sieve MD small M4 CPU fixed work; no convergence claim; score 6.99e-03
Neural sieve MD small M4 Metal fixed work; no convergence claim; score 6.99e-03
Neural sieve MD small Intel CPU fixed work; no convergence claim; score 6.99e-03
Neural sieve MD small RTX 5070 CUDA fixed work; no convergence claim; score 6.99e-03
Neural sieve MD medium M4 CPU fixed work; no convergence claim; score 8.39e-03
Neural sieve MD medium M4 Metal fixed work; no convergence claim; score 8.39e-03
Neural sieve MD medium Intel CPU fixed work; no convergence claim; score 8.39e-03
Neural sieve MD medium RTX 5070 CUDA fixed work; no convergence claim; score 8.39e-03
Neural sieve MD large M4 CPU fixed work; no convergence claim; score 1.17e-02
Neural sieve MD large M4 Metal fixed work; no convergence claim; score 1.17e-02
Neural sieve MD large Intel CPU fixed work; no convergence claim; score 1.17e-02
Neural sieve MD large RTX 5070 CUDA fixed work; no convergence claim; score 1.17e-02
Synthetic DID small M4 CPU iteration cap reached
Synthetic DID small M4 Metal iteration cap reached
Synthetic DID small Intel CPU iteration cap reached
Synthetic DID small RTX 5070 CUDA iteration cap reached
Synthetic DID medium M4 CPU iteration cap reached
Synthetic DID medium M4 Metal iteration cap reached
Synthetic DID medium Intel CPU iteration cap reached
Synthetic DID medium RTX 5070 CUDA iteration cap reached
Synthetic DID large M4 CPU iteration cap reached
Synthetic DID large M4 Metal iteration cap reached
Synthetic DID large Intel CPU iteration cap reached
Synthetic DID large RTX 5070 CUDA iteration cap reached

For linear MMR/sieve MD, the public interface does not expose the inner LBFGS iteration count; the recorded score is more informative than the length of its outer loss history. TWFE’s reported single solve does not count absorption passes. Dynamic-choice standard errors are not implemented and are not timed. Matrix completion’s relative-objective stop is not a primal–dual gap certificate.

Why a backend may not run

MPS CPU fallback is disabled. A result labeled Metal must not quietly execute its unsupported tensor operation on CPU and then be described as a native GPU benchmark.

Workload Sizes Observed failure
Weighted least squares large, medium, small MPS SVD operation unavailable
Two-way fixed effects large, medium, small MPS has no float64
Two-step IV GMM large, medium, small MPS has no float64
Matrix completion large, medium, small MPS SVD operation unavailable
Gaussian latent factors large, medium, small MPS least-squares operation unavailable
NFXP large, medium, small MPS has no float64
Hotz–Miller large, medium, small MPS has no float64

Float64 is unavailable in MPS; Trex’s FE absorption, Torch GMM and these dynamic choice implementations use it. Even float32 is not sufficient for every path: WLS validates matrix rank via SVD, matrix completion uses SVD directly, and the latent-factor initializer calls least squares. Support here refers specifically to Torch 2.9.1 and these estimator paths, not to every future Metal release. The benchmark does not patch a solver or silently move an operation to CPU.

Measurement boundaries and interpretation

Each case runs in a separate process, with both backends on a host measured serially. CUDA/MPS are synchronized at timing boundaries. The warm-up executes the same full fit and initializes kernels/libraries before the three timed repetitions. Every repetition constructs a fresh estimator; a previously fitted model is never reused. The case timeout is 240 seconds for imports, warm-up, all repetitions and diagnostics together, not a per-fit time limit.

There is no automatic mixed precision, TF32, torch.compile, or backend-specific solver tuning. Host-to-device input conversion and result transfers are timed; kernel-only and permanently GPU-resident workflows can have different tradeoffs. The download also reports fit-only and preparation times. “Fit-only” still includes conversions, validation or host work performed inside a public fit. For example, GMM is not a claim of fully GPU-resident inference simply because its optimization backend is Torch/CUDA.

The results explain workload behavior, not peak hardware throughput:

  • Tiny problems often finish on CPU before GPU dispatch and synchronization overhead can be amortized. Repeated Python control flow and scalar stopping checks can be especially expensive on a GPU.
  • Larger matrix products and batched neural computations can amortize those costs. Dense-kernel MMR also changes both arithmetic and memory demand as n^2, unlike a fixed-width linear regression.
  • Iterative solvers can take different numbers of steps at different sizes. A nonmonotone timing curve is not, by itself, a benchmark error. Consult the recorded iteration counts, scores and raw ranges rather than fitting a throughput curve to three points.
  • These are modest research-scale problems on one M4 and one consumer CUDA GPU, with one DGP draw per size. They do not establish the fastest solver, best thread count, energy efficiency, peak-memory frontier, or performance on A100/H100-class hardware. GAN/diffusion training, language models, GEL’s SciPy path and the full space of Trex options are outside this suite.

Download and reproduce

Install the pinned source and matching numerical-library versions in an appropriate environment on each host. Generate the data once and copy the whole directory; regenerating independently is not the matching procedure.

python benchmarks/devices.py generate --data tmp/device-bench/data

# Mac, after copying the exact generated directory:
python benchmarks/devices.py run --host mac --devices cpu,mps \
  --data tmp/device-bench/data --output tmp/device-bench/mac-results

# CUDA host, using those same input files:
python benchmarks/devices.py run --host fwk --devices cpu,cuda \
  --data tmp/device-bench/data --output tmp/device-bench/fwk-results

Collect the two result directories onto one machine and run benchmarks/summarize_devices.py as described in the benchmark README. The documentation build validates and analyzes saved measurements; GitHub’s CPU-only runner does not rerun or manufacture the hardware experiment. New hardware results should receive a new dated archive, preserving the source and environment associated with this snapshot.