SPX Comparison

Performance, Convergence, and Likelihood Validation: R pomp vs. pypomp

Published

June 24, 2026

Introduction

This report compares the performance, parameter convergence, and likelihood evaluation of the S&P 500 (SPX) stochastic volatility model used in Sun (2024) using the pypomp package (on CPU and GPU) and the R pomp package (on CPU).

The SPX model is a stochastic volatility model with the following parameter set:

  • \(\mu\): Expected rate of return of the asset.
  • \(\kappa\): Mean rate of reversion in variance.
  • \(\theta\): Long term average variance.
  • \(\xi\): Volatility coefficient of volatility.
  • \(\rho\): Leverage effect parameter (correlation between return and volatility shocks).
  • \(V_0\): Initial variance.

Because the SPX model uses simple random number generation (sampling from a normal distribution) and features a single rproc step per observation, it is highly sensitive to framework overhead in the IF2 optimization and particle filter (pfilter) algorithms. This benchmark compares runtimes, convergence trajectories, and likelihood distributions to verify the mathematical correctness and efficiency of pypomp.


Setup

── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.2     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.1.0     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Attaching package: 'pomp'


The following object is masked from 'package:purrr':

    map
R version 4.4.0 (2024-04-24)
Platform: x86_64-pc-linux-gnu
Running under: Red Hat Enterprise Linux 8.10 (Ootpa)

Matrix products: default
BLAS:   /sw/pkgs/arc/stacks/gcc/13.2.0/R/4.4.0/lib64/R/lib/libRblas.so 
LAPACK: /sw/pkgs/arc/stacks/gcc/13.2.0/R/4.4.0/lib64/R/lib/libRlapack.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: America/Detroit
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] pomp_6.3          lubridate_1.9.4   forcats_1.0.0     stringr_1.5.1    
 [5] dplyr_1.1.4       purrr_1.1.0       readr_2.1.5       tidyr_1.3.1      
 [9] tibble_3.3.0      ggplot2_3.5.2     tidyverse_2.0.0   reticulate_1.42.0

loaded via a namespace (and not attached):
 [1] Matrix_1.7-0       gtable_0.3.6       jsonlite_2.0.0     compiler_4.4.0    
 [5] renv_1.0.11        tidyselect_1.2.1   Rcpp_1.1.0         scales_1.4.0      
 [9] png_0.1-8          yaml_2.3.10        fastmap_1.2.0      here_1.0.1        
[13] coda_0.19-4.1      lattice_0.22-6     R6_2.6.1           generics_0.1.4    
[17] knitr_1.50         rprojroot_2.0.4    tzdb_0.5.0         pillar_1.11.0     
[21] RColorBrewer_1.1-3 rlang_1.1.6        deSolve_1.40       stringi_1.8.7     
[25] xfun_0.52          timechange_0.3.0   cli_3.6.5          withr_3.0.2       
[29] magrittr_2.0.3     digest_0.6.37      grid_4.4.0         mvtnorm_1.3-3     
[33] hms_1.1.3          lifecycle_1.0.4    vctrs_0.6.5        data.table_1.17.8 
[37] evaluate_1.0.4     glue_1.8.0         farver_2.1.2       rmarkdown_2.29    
[41] tools_4.4.0        pkgconfig_2.0.3    htmltools_0.5.8.1 
[1] "2026-06-24 12:10:11 EDT"

Metadata & Execution Runtimes

GPU Pomp Object Metadata:
Model Initialization Metadata:
------------------------------
pypomp version: 0.4.6.0
JAX version:    0.9.0.1
Python version: 3.12.13
Platform info:  Linux-4.18.0-553.123.1.el8_10.x86_64-x86_64-with-glibc2.28
Default device: gpu
Created at:     2026-06-16T19:32:29.339498+00:00

CPU Pomp Object Metadata:
Model Initialization Metadata:
------------------------------
pypomp version: 0.4.6.0
JAX version:    0.9.0.1
Python version: 3.12.13
Platform info:  Linux-4.18.0-553.123.1.el8_10.x86_64-x86_64-with-glibc2.28
Default device: cpu
Created at:     2026-06-16T19:32:30.337740+00:00

Performance & Throughput Comparison

The table below presents the execution runtimes, speedup factors, and throughput of each configuration relative to R pomp. The GPU used is a NVIDIA V100, and the CPU used is a 3.0 GHz Intel Xeon Gold 6154.

Configuration IF2 Time IF2 Speedup Pfilter Time Pfilter Speedup Total Time Total Speedup Throughput (vs 1 CPU Core)
R pomp (CPU, 36 Cores) 6893.52s (114.89m) 1.00x 289.63s (4.83m) 1.00x 7183.41s (119.72m) 1.00x 36.00x
pypomp (CPU, 36 Cores) 3226.21s (53.77m) 2.14x 141.54s (2.36m) 2.05x 3367.75s (56.13m) 2.13x 76.79x
pypomp (GPU, 1 V100 GPU) 372.22s (6.20m) 18.52x 40.59s (0.68m) 7.14x 412.82s (6.88m) 17.40x 626.43x
Performance Expectations
  • GPU Acceleration: We expect pypomp on the V100 GPU to achieve at least a 16x speedup over the 36-core CPU reference R run.
  • CPU JAX Overhead: pypomp on the CPU (using JAX) achieve at least a 2x speedup over the R run.
  • Throughput: Throughput measures the equivalent serial CPU core count required to match the execution speed. A GPU run should demonstrate throughput equivalent to hundreds of CPU cores.

Parameter Convergence & Traces (IF2)

Comparison of Python and R Parameter Traces

Here, we overlay the trajectories from R, Python (CPU), and Python (GPU) to compare the search paths directly.

Expectation

Since pypomp is designed to be a direct Python/JAX implementation of R’s pomp package, the paths taken by the individual replicates in Python (both CPU and GPU) should be similar to those in R, overlapping substantially.


Comparison of Python and R Parameter Trace Quantiles

To abstract away the noise of individual runs, this plot compares the 10th and 90th percentile bounds of the parameters across all runs.

Expectation

While comparing individual noisy trajectories can be difficult, the 10th and 90th percentile ribbons provide a more robust view of convergence. If the package is working properly, the ribbons for Python (GPU), Python (CPU), and R should overlap closely throughout the iterations.


Parameter Estimates Distribution

Comparison of Final Parameter Estimates

This plot compares the distribution of final estimated parameters across replicates.

Expectation

The density of the final parameter values (at the last iteration) across replicates should overlap heavily for each configuration.


Comparison of Starting Parameter Values (Sanity Check)

This plot shows the distribution of initial parameter values to verify that the configurations started from identical distributions.

Expectation

This density plot serves as a sanity check. Because the CPU, GPU, and R runs are initialized using the same random starting box, the starting densities should be be very similar across the configurations.


Log-Likelihood Comparisons

Summary of Final Log-Likelihood Estimates

The table below summarizes the log-likelihood (LL) statistics across replicates at the final iteration.

Configuration Min Median Mean Max SD
pypomp (GPU) 1.082571e+04 11841.80 1.178364e+04 11850.08 2.335500e+02
pypomp (CPU) -9.814143e+30 11841.58 -2.726151e+28 11852.80 5.172507e+29
R pomp -7.886362e+28 11841.38 -2.190656e+26 11849.36 4.156478e+27

Python (GPU) Top 10:

         LL        sd
1  11850.08 0.5501608
2  11849.95 0.8087098
3  11849.69 0.8691682
4  11849.62 0.9652821
5  11849.35 0.6062785
6  11849.21 1.3459180
7  11849.09 1.3932870
8  11848.95 0.7935572
9  11848.84 1.3342843
10 11848.76 0.4291984

Python (CPU) Top 10:

         LL        sd
1  11852.80 5.4839632
2  11852.40 2.9185789
3  11850.47 4.8009773
4  11850.13 3.8902133
5  11849.47 0.6087979
6  11849.36 0.5509179
7  11849.35 0.4042187
8  11849.17 0.6684040
9  11849.04 1.3936672
10 11848.88 1.2137035

R Top 10:

                est        se
result.155 11849.36 0.3611692
result.292 11849.29 2.3713004
result.184 11849.03 0.6089239
result.212 11848.95 0.7779408
result.284 11848.90 1.1797977
result.127 11848.84 0.4285825
result.43  11848.72 0.7901084
result.220 11848.65 0.4942647
result.334 11848.53 2.3266285
result.210 11848.37 1.4432874

Density Plot Comparison (LL > 11,800)

This plot shows the density distribution of final log-likelihood estimates.

Expectation

We would like the final log-likelihood estimates from each configuration to be identical, so we want to see all 3 density estimates overlap heavily.


Likelihood Evaluation at Fixed Parameter (rep3600 Check)

This section evaluates the particle filter’s likelihood calculation itself by running it multiple times on a single, fixed parameter vector (Sun (2024) estimates).

Expectation

Again, we want the two density curves to be about the same, overlapping heavily.


This table compares the parameter estimates reported in Sun (2024) to our best Python (GPU) parameter estimates.

quantity param_value python_value Weizhe_value difference
mu 0.0004 -7.7555 -7.9074 0.1519
kappa 0.0315 -3.4580 -3.4609 0.0030
theta 0.0001 -9.1970 -9.0970 -0.1000
xi 0.0023 -6.0827 -6.0880 0.0053
rho -0.7656 -2.0192 -1.8921 -0.1270
V_0 0.0000 -10.8398 -9.7435 -1.0963

References

Sun, Weizhe. 2024. “Model Based Inference of Stochastic Volatility via Iterated Filtering.” Undergraduate Honors Thesis, University of Michigan.