Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8a8fd9a
POC: parallelize r.proj via RAM-resident buffer, 2.5x speedup on 8-co…
Mar 16, 2026
36a5789
r.proj: use memory option to limit RAM buffer per community feedback
Mar 16, 2026
4ddf520
r.proj: replace full-map RAM buffer with memory-bounded banding
krcoder123 Jul 2, 2026
8420d2c
r.proj: link libproj for direct proj_* calls
krcoder123 Jul 2, 2026
781e9bf
r.proj: address review comments (remove stray PACKAGE define, clarify…
krcoder123 Jul 3, 2026
eda6d1e
r.proj: guard OpenMP timer calls for non-OpenMP builds
krcoder123 Jul 8, 2026
266f7fe
lib/proj: add per-thread transform clone helpers
krcoder123 Jul 9, 2026
e2a7d9f
r.proj: experimental per-thread-fd parallel strip reads
krcoder123 Jul 8, 2026
a42445f
r.proj: add adaptive column tiling for oblique reprojections
krcoder123 Jul 10, 2026
70b6f1c
r.proj: reuse previous band size in the tile sizing search
krcoder123 Jul 13, 2026
c621ef3
r.proj: size the input strip for tiles containing a pole
krcoder123 Jul 14, 2026
2f5747e
r.proj: add strip-based non-nearest interpolation to the banded path
krcoder123 Jul 18, 2026
03cdd97
r.proj: fix output row northing mismatch with serial
krcoder123 Jul 18, 2026
a60cfa5
r.proj: fall back to serial tile cache instead of aborting
krcoder123 Jul 18, 2026
5419dd8
lib/proj: add NULL checks to GPJ_clone_transform
krcoder123 Jul 21, 2026
f23d979
r.proj: fix band sizing for pole-centered frames reading a truncated …
krcoder123 Jul 21, 2026
c264795
r.proj: add parallel-correctness pytest tests
krcoder123 Jul 21, 2026
0d6747e
r.proj: keep the input strip resident across consecutive bands
krcoder123 Jul 22, 2026
6336e24
r.proj: overlap the output write with the next band's compute
krcoder123 Jul 22, 2026
fb7b8fe
r.proj: add nprocs option and clean up comments
krcoder123 Jul 23, 2026
0826fae
r.proj: add thread scaling benchmark script
krcoder123 Jul 23, 2026
3874ab7
r.proj: add footprint grid measurement alongside the fit search
krcoder123 Aug 1, 2026
f86ff66
r.proj: size band heights from the footprint grid
krcoder123 Aug 1, 2026
3c15ccc
r.proj: size tile widths from the footprint grid
krcoder123 Aug 1, 2026
da0991b
r.proj: match the serial rounding for large integer values
krcoder123 Aug 2, 2026
da8019b
r.proj: remove the grid verification scaffolding
krcoder123 Aug 2, 2026
8b73c2b
r.proj: rename the sizing variables for clarity
krcoder123 Aug 2, 2026
fb6ee01
r.proj: shorten and clarify the comments
krcoder123 Aug 3, 2026
9b20905
r.proj: co-size band heights and tile widths from the footprint grid
krcoder123 Aug 4, 2026
a32fa3a
r.proj: shorten the test and benchmark comments
krcoder123 Aug 6, 2026
c125e33
r.proj: reload an undersized strip instead of aborting
krcoder123 Aug 14, 2026
9ec2bde
r.proj: add a test for the strip reload
krcoder123 Aug 14, 2026
6c99bbf
r.proj: move the footprint code behind a small fp interface
krcoder123 Aug 14, 2026
e5bed46
r.proj: create the fallback cache temp file in the output project
krcoder123 Aug 14, 2026
7124297
r.proj: harden the parallel tests
krcoder123 Aug 14, 2026
c8128fb
r.proj: rename the footprint fields and simplify the block lookup
krcoder123 Aug 19, 2026
fb4f7db
r.proj: apply the footprint review renames and comments
krcoder123 Aug 22, 2026
64ca6ed
r.proj: use the merged test fixtures
krcoder123 Aug 22, 2026
9e7b30c
r.proj: add the kernel table check and document nprocs and memory
krcoder123 Aug 23, 2026
d6e3083
Merge branch 'main' into rproj-footprint-sizing
krcoder123 Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/grass/defs/gprojects.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ int GPJ_transform(const struct pj_info *, const struct pj_info *,
int GPJ_transform_array(const struct pj_info *, const struct pj_info *,
const struct pj_info *, int, double *, double *,
double *, int);
void GPJ_clone_transform(const struct pj_info *, struct gpj_transform_clone *);
void GPJ_free_transform_clone(struct gpj_transform_clone *);

/* old API, to be removed */
int pj_do_proj(double *, double *, const struct pj_info *,
Expand Down
9 changes: 9 additions & 0 deletions include/grass/gprojects.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ struct pj_info {
char *wkt;
};

/* Per-thread clone of a transform, filled by GPJ_clone_transform() and
* released by GPJ_free_transform_clone(). Bundles the cloned transform with
* the private PROJ context it was cloned into, so ownership is a single unit.
*/
struct gpj_transform_clone {
struct pj_info info;
PJ_CONTEXT *ctx;
};

struct gpj_datum {
char *name, *longname, *ellps;
double dx, dy, dz;
Expand Down
43 changes: 43 additions & 0 deletions lib/proj/do_proj.c
Original file line number Diff line number Diff line change
Expand Up @@ -1414,3 +1414,46 @@ int pj_do_transform(int count, double *x, double *y, double *h,
}
return ok;
}

/*!
* \brief Clone a transform into a fresh per-thread PROJ context
*
* PROJ transformation objects are not safe for concurrent use, so each thread
* needs its own. This fills \p clone with a copy of \p src whose PJ is cloned
* into a new private context. Release it with GPJ_free_transform_clone().
*
* Safe to call concurrently from multiple threads with the same \p src,
* provided \p src is not modified during the calls. Each call clones into its
* own new context and touches no shared mutable state.
*
* \param src source transform (as set up by GPJ_init_transform())
* \param[out] clone receives the per-thread clone (info plus private context)
*/
void GPJ_clone_transform(const struct pj_info *src,
struct gpj_transform_clone *clone)
{
clone->ctx = proj_context_create();
/* A failed context leaves the thread with no usable transform, so this
* aborts the run. */
if (clone->ctx == NULL)
G_fatal_error(_("proj_context_create() failed for a per-thread "
"transform clone"));
clone->info = *src;
clone->info.pj = proj_clone(clone->ctx, src->pj);
if (clone->info.pj == NULL)
G_fatal_error(_("proj_clone() failed for a per-thread transform "
"clone"));
}

/*!
* \brief Free a per-thread transform clone and its context
*
* \param clone clone filled by GPJ_clone_transform(); its cloned PJ is set to
* NULL after release
*/
void GPJ_free_transform_clone(struct gpj_transform_clone *clone)
{
proj_destroy(clone->info.pj);
proj_context_destroy(clone->ctx);
clone->info.pj = NULL;
}
128 changes: 128 additions & 0 deletions raster/r.proj/benchmark/benchmark_r_proj.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""This is a benchmark script for r.proj thread scaling with grass.benchmark.

This script sweeps through a raster size at a fixed memory setting, and then
memory at a fixed size. It then plots time, speedup, and efficiency. Creates
its own source raster and projects, so it runs standalone with
grass --exec python benchmark_r_proj.py."""

import os
import tempfile

from grass.exceptions import CalledModuleError, GrassError
from grass.pygrass.modules import Module
import grass.script as gs
import grass.benchmark as bm

# Baselines held fixed while one dimension is swept.
BASE_MAPSIZE = 50e6 # cells
BASE_MEMORY = 300 # MB
MAPSIZES = [10e6, 50e6, 100e6]
MEMORIES = [50, 100, 300, 1000]
METRICS = ["time", "speedup", "efficiency"]
MAX_NPROCS = 8
REPEAT = 3

SRC_PROJECT = "src4326"
DST_PROJECT = "dst3857"
INPUT = "benchmark_r_proj_reference"
OUTPUT = "benchmark_r_proj"


def main():
gisdbase = tempfile.mkdtemp(prefix="bench_r_proj_")
gs.create_project(os.path.join(gisdbase, SRC_PROJECT), epsg="4326")
gs.create_project(os.path.join(gisdbase, DST_PROJECT), epsg="3857")

# Sweep raster size at the baseline memory.
results = []
for mapsize in MAPSIZES:
benchmark(
gisdbase,
size=int(mapsize**0.5),
memory=BASE_MEMORY,
label=f"r.proj_{int(mapsize / 1e6)}M",
results=results,
)
plot(results, "rastersize")

# Sweep memory at the baseline raster size.
results = []
for memory in MEMORIES:
benchmark(
gisdbase,
size=int(BASE_MAPSIZE**0.5),
memory=memory,
label=f"r.proj_memory_{memory}MB",
results=results,
)
plot(results, "memory")


def benchmark(gisdbase, size, memory, label, results):
generate_input(gisdbase, size)
with gs.setup.init(
os.path.join(gisdbase, DST_PROJECT), env=os.environ.copy()
) as session:
env = session.env
# Output region from r.proj's own suggested bounds for this input.
text = gs.read_command(
"r.proj",
project=SRC_PROJECT,
mapset="PERMANENT",
dbase=gisdbase,
input=INPUT,
method="nearest",
flags="g",
env=env,
)
region = dict(token.split("=") for token in text.split())
gs.run_command("g.region", env=env, **region)

module = Module(
"r.proj",
project=SRC_PROJECT,
mapset="PERMANENT",
dbase=gisdbase,
input=INPUT,
output=OUTPUT,
method="nearest",
memory=memory,
env_=env,
run_=False,
overwrite=True,
)
results.append(
bm.benchmark_nprocs(
module, label=label, max_nprocs=MAX_NPROCS, repeat=REPEAT
)
)


def generate_input(gisdbase, size):
"""Generate the source raster in the EPSG:4326 project. Uses
r.surf.fractal, or r.random.surface when FFTW is unavailable."""
with gs.setup.init(
os.path.join(gisdbase, SRC_PROJECT), env=os.environ.copy()
) as session:
env = session.env
gs.run_command(
"g.region", n=50, s=40, w=-110, e=-90, rows=size, cols=size, env=env
)
try:
Module("r.surf.fractal", output=INPUT, overwrite=True, env_=env)
except (CalledModuleError, GrassError):
Module("r.random.surface", output=INPUT, overwrite=True, env_=env)


def plot(results, sweep):
for metric in METRICS:
bm.nprocs_plot(
results,
filename=f"r_proj_{sweep}_{metric}.svg",
title=f"r.proj {sweep} {metric}",
metric=metric,
)


if __name__ == "__main__":
main()
Loading
Loading