Performance tuning#
Overview#
solve() parallelises the block loop with OpenMP and caches Cholesky
factorizations to avoid redundant O(nmax³) linear-algebra work. Two
parameters on solve() control both:
k.solve(nthread=4, ncache=64)
Understanding how they interact lets you get the best wall time for your problem size.
The factor cache#
Every kriging system is defined by the exact set of neighbour indices that fall inside the search radius for a given block. Once the Cholesky factorization of that covariance matrix has been computed (O(nmax³)), it can be reused for any later block that has the same neighbour set, reducing the per-block cost to a triangular solve (O(nmax²)).
All OpenMP threads share one bounded, set-associative hash cache containing up
to ncache factorizations (default 256 total slots, set at compile time).
Per-bucket locks let one thread reuse a factorization produced by another
without serializing accesses to unrelated neighbourhoods.
Measuring cache effectiveness#
After every solve() call, solver_stats reports the hit/miss counts:
k.solve()
stats = k.solver_stats
hit_rate = stats["chol_reuse"] / (stats["chol_fact"] + stats["chol_reuse"])
print(f"Cache hit rate: {hit_rate:.1%}")
# chol_fact — fresh O(nmax³) factorizations
# chol_reuse — O(nmax²) solves via a cached factor
A hit rate above ~80 % means the cache is working well and tuning can squeeze out further gains. A hit rate near zero means every block has a unique neighbourhood and the cache is not helping.
nthread — number of OpenMP threads#
k.solve(nthread=0) # 0 (default): leave OMP_NUM_THREADS unchanged
k.solve(nthread=1) # force single-threaded
k.solve(nthread=4) # use exactly 4 threads
The block loop uses dynamic scheduling. The shared cache preserves factorization reuse even when matching neighbourhoods are processed by different workers.
When to reduce nthread#
The main reasons to reduce threads are scheduling overhead and memory-bandwidth contention. This matters when:
nmaxis large — factorization and solve traffic can saturate available memory bandwidth.The grid is small — when the total number of blocks is close to the thread count, the overhead of spawning threads can outweigh the parallelism benefit.
Example — small obs dataset, large grid:
# 30 observations, 50 000 grid blocks, nmax=20
# Most blocks share the same 20 nearest neighbours.
k.solve(nthread=2) # try fewer threads
stats = k.solver_stats
print(stats["chol_fact"], stats["chol_reuse"])
# Tune nthread for elapsed time; the shared cache avoids cross-thread duplicates.
When to keep nthread high#
More threads usually help when:
The observation dataset is large and varied — each block has a largely unique neighbourhood, so the cache barely fires anyway.
The grid is large and
nmaxis small — per-block cost is low, and parallelism savings dominate over any cache benefit.
Quick-reference decision table#
Situation |
Recommendation |
|---|---|
Small obs (< ~100), large grid, high |
Benchmark |
Large obs, large grid |
Keep |
All obs fit |
|
Hit rate low despite high |
Neighbourhoods are all unique; cache cannot help |
Hit rate high but |
Increase |
Memory-constrained with large |
Reduce |
SGSIM |
|