Skip to content

Lazy neighborhood filter kernel compilation - #1708

Open
cmdupuis3 wants to merge 3 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy_nb_kernels
Open

Lazy neighborhood filter kernel compilation#1708
cmdupuis3 wants to merge 3 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy_nb_kernels

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #1706

Overview

This PR should solve an issue introduced with PR #941, wherein the neighborhood filter gufunc-based kernels would compile eagerly, forcing a major slowdown of basic import uxarray imports, as well as creating threadpools that would eventually block safe forking that is needed in PR #1700.

The idea is to have lazy compilation of gufuncs introduced with neighborhood filters for performant reductions. This prevents the otherwise eager compilation of every one of these kernels upon import uxarray.

Combined, this PR and PR #1700 can bring the total benchmark suit runtime down to about half of the pre-neighborhood filters runtime of ~40min, yielding about 20min without neighborhood filter benchmarks and 26-40ish minutes with them.

On its own, this PR can at least mitigate some of the benchmark performance regression, on the order of about 30-50% or so.

PR Checklist

General

  • An issue is created and linked
  • Added appropriate labels (if your uxarray repo permissions allow it)
  • Filled out Overview and Expected Usage (if applicable) sections

Testing & Benchmarking

  • Adequate tests are created if there is new functionality
  • Tests are not too basic (such as simply calling a function and nothing else)
  • Tests cover all major paths in your new functions
  • If this PR could affect performance, ran ASV benchmarks and confirmed they show expected behavior (add a new benchmark if necessary)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have been updated with any function changes

AI Disclosure

AI Usage: Claude Opus 5

  • I take responsibility for all AI-generated content in my PR.
  • I have tested all AI-generated content in my PR.

@cmdupuis3
cmdupuis3 requested a review from Sevans711 August 25, 2026 20:34
@cmdupuis3 cmdupuis3 self-assigned this Aug 25, 2026
@cmdupuis3 cmdupuis3 added bug Something isn't working run-benchmark Run ASV benchmark workflow benchmarking Related to benchmarks, memory usage, and/or time profiling labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [c832ff1] After [69ddea0] Ratio Benchmark (Parameter)
- 410M 336M 0.82 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
- 440M 368M 0.84 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
- 411M 338M 0.82 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 410M 337M 0.82 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 11.9±0.03s 844±7ms 0.07 import.Imports.timeraw_import_uxarray
- 370M 293M 0.79 import.Imports.track_peakmem_import_uxarray
- 429M 356M 0.83 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 1)
- 431M 356M 0.83 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 2)
- 429M 356M 0.83 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 4)
- 412M 339M 0.82 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 1)
- 412M 340M 0.82 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 2)
- 412M 339M 0.82 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 4)
- 424M 352M 0.83 mpas_ocean.GradientColdStartRss.peakmem_gradient('120km')
- 403M 331M 0.82 mpas_ocean.GradientColdStartRss.peakmem_gradient('480km')
- 16.0±0.5ms 12.6±0.2ms 0.79 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
- 12.3±0.5ms 9.19±0.1ms 0.74 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
- 431M 358M 0.83 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('120km')
- 414M 341M 0.82 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('480km')

Benchmarks that have stayed the same:

Change Before [c832ff1] After [69ddea0] Ratio Benchmark (Parameter)
195±0.6ms 205±10ms 1.05 bench_connectivity.Connectivity.time_edge_face('120km')
12.0±0.09ms 12.0±0.04ms 1.00 bench_connectivity.Connectivity.time_edge_face('480km')
203±10ms 193±1ms 0.95 bench_connectivity.Connectivity.time_edge_node('120km')
10.9±0.02ms 11.0±0.05ms 1.01 bench_connectivity.Connectivity.time_edge_node('480km')
193±1ms 195±0.6ms 1.01 bench_connectivity.Connectivity.time_face_edge('120km')
11.2±0.04ms 11.3±0.2ms 1.00 bench_connectivity.Connectivity.time_face_edge('480km')
879±4ms 892±10ms 1.01 bench_connectivity.Connectivity.time_face_face('120km')
56.6±1ms 57.4±0.2ms 1.01 bench_connectivity.Connectivity.time_face_face('480km')
72.1±7μs 68.4±2μs 0.95 bench_connectivity.Connectivity.time_face_node('120km')
69.7±3μs 71.6±3μs 1.03 bench_connectivity.Connectivity.time_face_node('480km')
423±8μs 425±10μs 1.01 bench_connectivity.Connectivity.time_n_nodes_per_face('120km')
359±9μs 362±9μs 1.01 bench_connectivity.Connectivity.time_n_nodes_per_face('480km')
192±0.6ms 193±0.9ms 1.01 bench_connectivity.Connectivity.time_node_edge('120km')
11.3±0.02ms 11.4±0.03ms 1.01 bench_connectivity.Connectivity.time_node_edge('480km')
90.5±2ms 90.0±1ms 0.99 bench_connectivity.Connectivity.time_node_face('120km')
5.50±0.1ms 5.22±0.06ms 0.95 bench_connectivity.Connectivity.time_node_face('480km')
8.63±0.1ms 8.62±0.08ms 1.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.77±0.04ms 2.76±0.06ms 1.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
10.6±10s 10.3±10ms ~0.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.24±0.03ms 2.18±0.03ms 0.97 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
57.3k 57.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
12.3k 12.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
123k 123k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
128 128 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.27M 1.27M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
50.1k 50.1k 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
1.48M 1.48M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
712 712 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.96M 1.98M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.97M 1.98M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
2.13M 2.15M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
38.3k 38.3k 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.24±0.03μs 1.26±0.04μs 1.02 geometry_kernels.AccucrossKernels.time_accucross
2.74±0.02μs 2.76±0.02μs 1.01 geometry_kernels.AccucrossKernels.time_accucross_pair
445±9ns 470±20ns 1.06 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
425±9ns 441±9ns 1.04 geometry_kernels.EFTPrimitives.time_diff_of_products
511±700ns 386±10ns ~0.75 geometry_kernels.EFTPrimitives.time_two_prod
396±20ns 386±20ns 0.97 geometry_kernels.EFTPrimitives.time_two_sum
1.63±0.03μs 1.57±0.05μs 0.96 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
1.19±0.03μs 1.13±0.02μs 0.95 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
2.02±0.06μs 2.00±0.04μs 0.99 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
1.76±0.09μs 1.72±0.03μs 0.98 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
1.43±0.02μs 1.42±0.02μs 0.99 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
2.19±0.01μs 2.19±0.05μs 1.00 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
53.8±0.8μs 50.6±0.8μs 0.94 geometry_kernels.OrientPredicates.time_on_minor_arc
1.12±0.03μs 1.11±0.06μs 0.99 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
2.75±0.1ms 2.60±0.02ms 0.94 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.17±0ms 1.17±0ms 1.00 geometry_samebody.SameBodyConstLat.time_accux_kernel
1.73±0.01ms 1.92±0.2ms ~1.11 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
149±1μs 153±4μs 1.03 geometry_samebody.SameBodyConstLat.time_fp64_kernel
32.2±0.08ms 32.8±0.3ms 1.02 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
10.2±0.01ms 10.3±0.07ms 1.01 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
26.5±0.01ms 26.4±0ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
4.86±0.01ms 4.88±0.02ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
2.73±0.06ms 2.81±0.05ms 1.03 mpas_ocean.CheckNorm.time_check_norm('120km')
2.32±0.05ms 2.25±0.05ms 0.97 mpas_ocean.CheckNorm.time_check_norm('480km')
823±10ms 844±7ms 1.03 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
53.9±0.9ms 54.0±0.2ms 1.00 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
673±7μs 684±10μs 1.02 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
608±10μs 616±8μs 1.01 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.51±0.05ms 5.50±0.02ms 1.00 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.95±0.04ms 4.02±0.04ms 1.02 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
99.7±0.6ms 99.6±0.3ms 1.00 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
10.7±0.03ms 10.7±0.3ms 1.00 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.2±0.03ms 18.2±0.02ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.06±0.02ms 1.06±0ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.7±0.02ms 10.6±0.02ms 1.00 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
755±20μs 752±8μs 1.00 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
589±4ms 593±10ms 1.01 mpas_ocean.CrossSections.time_const_lat('120km', 1)
295±1ms 302±3ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 2)
154±0.8ms 155±1ms 1.01 mpas_ocean.CrossSections.time_const_lat('120km', 4)
535±2ms 531±4ms 0.99 mpas_ocean.CrossSections.time_const_lat('480km', 1)
264±2ms 268±6ms 1.02 mpas_ocean.CrossSections.time_const_lat('480km', 2)
139±0.6ms 139±1ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 4)
25.3±0.3ms 26.2±0.6ms 1.03 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
3.28±0.08ms 3.36±0.07ms 1.02 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
62.2±0.4ms 63.1±0.7ms 1.02 mpas_ocean.FaceAreas.time_face_areas('120km')
4.99±5s 7.99±5ms ~0.00 mpas_ocean.FaceAreas.time_face_areas('480km')
229k 229k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('120km')
14.3k 14.3k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('480km')
2.12M 2.12M 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('120km')
817k 823k 1.01 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
949±10ms 951±20ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
52.9±0.7ms 53.0±0.9ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
85.0±0.8ms 84.4±1ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.76±0.06ms 5.89±0.2ms 1.02 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
175±0.5ms 181±3ms 1.03 mpas_ocean.Gradient.time_gradient('120km')
12.5±0.1ms 12.7±0.2ms 1.02 mpas_ocean.Gradient.time_gradient('480km')
457k 457k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('120km')
28.7k 28.7k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('480km')
5.08M 5.08M 1.00 mpas_ocean.Gradient.track_peakmem_gradient('120km')
328k 327k 1.00 mpas_ocean.Gradient.track_peakmem_gradient('480km')
383±10μs 396±9μs 1.03 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
215±9μs 213±20μs 0.99 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
600±20μs 595±20μs 0.99 mpas_ocean.Integrate.time_integrate('120km')
510±7μs 528±20μs 1.04 mpas_ocean.Integrate.time_integrate('480km')
18.4M 18.4M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('120km')
1.2M 1.2M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('480km')
185±1ms 185±1ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
184±1ms 187±1ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
186±0.9ms 186±1ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
14.1±0.1ms 14.5±0.5ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
13.9±0.1ms 14.6±0.2ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
14.1±0.2ms 14.2±0.2ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
239±1ms 238±0.7ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
1.28±0s 1.28±0s 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 15.0)
492±2ms 494±2ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
13.0±0.07ms 13.1±0.03ms 1.01 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
25.1±0.07ms 25.0±0.1ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 15.0)
16.2±0.03ms 16.1±0.07ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
234±0.6ms 234±0.3ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
1.25±0s 1.25±0s 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 15.0)
486±1ms 486±1ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
12.6±0.05ms 12.6±0.08ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
24.3±0.1ms 24.4±0.1ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 15.0)
15.8±0.1ms 15.7±0.03ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 5.0)
1.19 1.19 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 1.0)
612.76 612.76 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 15.0)
74.17 74.17 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 5.0)
1.0 1.0 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 1.0)
37.29 37.29 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 15.0)
6.57 6.57 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 5.0)
728k 728k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 1.0)
141M 141M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 15.0)
17.4M 17.4M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 5.0)
43k 43k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 1.0)
563k 563k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 15.0)
123k 123k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 5.0)
5.72M 5.72M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 1.0)
145M 145M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 15.0)
21.5M 21.5M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 5.0)
362k 362k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 1.0)
824k 825k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 15.0)
384k 384k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 5.0)
48.8±0.5ms 45.7±0.7ms 0.94 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
22.7±0.01ms 22.7±0.08ms 1.00 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
45.0±1ms 42.5±0.4ms 0.94 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
733±40μs 688±20μs 0.94 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
5.76M 5.82M 1.01 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'grid_chunks')
2.75M 2.75M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'numpy')
5.68M 5.67M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
177k 177k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
543k 525k 0.97 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
12.3±0s 12.4±0s 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
13.1±0.07s 13.0±0.01s 0.99 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
227±0.3ms 227±0.5ms 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
234±0.6ms 235±0.2ms 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
1.31±0s 1.31±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
1.50±0s 1.50±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
25.5±0.2ms 25.3±0.2ms 0.99 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
27.2±0.1ms 26.9±0.1ms 0.99 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
40.2±0.05ms 40.2±0.06ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
233±0.2ms 233±0.3ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
740±20μs 703±50μs 0.95 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
2.10±0.04ms 2.06±0.02ms 0.98 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'median')
239k 239k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'mean')
245k 245k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'median')
19.7k 19.3k 0.98 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'mean')
20.2k 19.8k 0.98 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'median')
412±20μs 416±7μs 1.01 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
406±20μs 400±10μs 0.98 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
391±10μs 378±20μs 0.97 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
362±9μs 356±4μs 0.98 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
232±0.5ms 236±0.7ms 1.02 mpas_ocean.RemapDownsample.time_bilinear_remapping
287±2ms 285±2ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
15.6±0.09ms 15.6±0.09ms 1.00 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.38±0.01s 1.37±0s 0.99 mpas_ocean.RemapUpsample.time_bilinear_remapping
36.7±0.1ms 38.3±0.5ms 1.04 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
12.7±0.09ms 12.6±0.09ms 1.00 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
10.5±0.1ms 10.4±0.1ms 0.99 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.81±0.05ms 5.91±0.3ms 1.02 mpas_ocean.ZonalAverage.time_zonal_average('480km')
6.94±0.1ms 6.97±0.1ms 1.00 quad_hexagon.QuadHexagon.time_open_dataset
5.82±0.1ms 5.79±0.06ms 0.99 quad_hexagon.QuadHexagon.time_open_grid
408 408 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_dataset
392 392 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_grid
73.8k 73.5k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
72.8k 72.8k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_grid

Benchmarks that have got worse:

Change Before [c832ff1] After [69ddea0] Ratio Benchmark (Parameter)
+ 686k 759k 1.11 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')

@Sevans711 Sevans711 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you looking into this! Assuming you wanted me to give an initial review now because you requested review, but not 100% sure since it is still marked as draft.

The regression test is clever, and I confirmed it crashes on main. This should be one good way to guard against import uxarray slowdowns in the future!

I left inline comments about the caching. Basically, it feels strange to me to use functools.cache in this situation, and I think it leads to some more-complicated-than-necessary code logic at the kernel call sites. I think it would be easier to maintain/understand quickly in the future if using a single cache which directly contains the cached kernels themselves, instead of caching on a per-function basis. That might also help with introspection/debugging later if you ever want to quickly check which kernels have been compiled so far. For example, there could even be a test which asserts _NUMBA_KERNELS is empty after import uxarray.

Comment thread uxarray/grid/neighbors.py Outdated
def mean(self, uxda):
"""Mean of each neighborhood."""
return self._apply_kernel(uxda, _MEAN_KERNEL, 0.0)
return self._apply_kernel(uxda, _mean_kernel, 0.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this isn't equivalent anymore; the equivalent syntax if sticking with functools here would be _mean_kernel(), right?

I would have a slight preference for a solution that doesn't use functools.cache, to avoid this confusion. Something like this would be more readable and less likely to cause typo, in my opinion:

_NUMBA_KERNELS = {}
def _numba_kernel(kernel_name):
    """returns numba kernel object, such as _make_kernel(lambda window, _: np.mean(window)).
    kernels are cached after initially being created.
    Supported kernels are: mean, sum, ...
    """
    if kernel_name in _NUMBA_KERNELS:
        return _NUMBA_KERNELS[kernel_name]
    # else, need to make kernel for the first time and cache it:
    if kernel_name=='mean':
        kernel = _make_kernel(lambda window, _: np.mean(window))
    elif kernel_name=='sum':
        kernel = _make_kernel(lambda window, _: np.sum(window))
    # ... similar pattern for all supported names
    else:
        raise ValueError(f'unrecognized kernel_name: {kernel_name!r}')
    _NUMBA_KERNELS[kernel_name] = kernel
    return kernel

Sidenote: upon initially reading the code I actually had the feeling that something like _NUMBA_KERNELS probably should actually be attached to the Neighborhood class for now, since these kernels are specifically used by the Neighborhood class and nowhere else. If you think these will always just be specific to the Neighborhood class, I'd recommend something like:

class Neighborhood():
    _NUMBA_KERNELS = {}

    @classmethod
    def _numba_kernel(cls, kernel_name):
        # same as above, but use cls._NUMBA_KERNELS instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the first commit on this branch took an OO approach that should be equivalent to the current approach, but I thought it was messier than the functools.cache implementation here. I think this approach has some of the same clarity issues, where we need a lot of OO and control flow to get a relatively simple result.

I'll move the kernels inside Neighborhood, and go from there.

Comment thread uxarray/grid/neighbors.py
if block.dtype not in (np.float64, np.float32):
block = block.astype(np.float64)
return kernel(block, *arrays, param)
return kernel()(block, *arrays, param)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see now that you added an extra call here… probably to deal with the issue I noted in my previous comment? I think this is a confusing abstraction though; basically, with this syntax it means that kernel isn't actually a compiled kernel at all, it is a "function factory which returns a compiled kernel function". This feels like it is returning to functional programming abstractions which I recall you mentioning you wanted to avoid.

If you dislike my previous suggestion and really want to keep the functools solution, I would really want to see the calls used above, e.g. self._apply_kernel(uxda, _mean_kernel(), 0.0) instead of here.

(If you really like the code as-is, the minimal change I would want to see would be to add clearer comments / docstrings to clarify for future developers that kernel isn't actually the kernel, but rather a function which returns a kernel function.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be okay, either way is fine.

To be specific, I was talking about getting away from the functional-style API, rather than necessarily the underlying mechanics. I think considering how numba and @guvectorize are interacting here, a functional approach at some level is unavoidable.

@cmdupuis3
cmdupuis3 marked this pull request as ready for review August 26, 2026 19:16
@cmdupuis3 cmdupuis3 mentioned this pull request Aug 26, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmarking Related to benchmarks, memory usage, and/or time profiling bug Something isn't working run-benchmark Run ASV benchmark workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Neighborhood filter reductions are making uxarray imports slow

2 participants