<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://grasswiki.osgeo.org/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Kaushikraja</id>
	<title>GRASS-Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://grasswiki.osgeo.org/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Kaushikraja"/>
	<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/wiki/Special:Contributions/Kaushikraja"/>
	<updated>2026-08-27T23:57:40Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29151</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29151"/>
		<updated>2026-08-24T06:12:12Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Log of Pull Requests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| Done&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || Closed in favor or #7807&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29150</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29150"/>
		<updated>2026-08-23T22:50:18Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Timeline */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| Done&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || Closed in favor or #7807&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29149</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29149"/>
		<updated>2026-08-23T13:15:32Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Timeline */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || Closed in favor or #7807&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29144</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29144"/>
		<updated>2026-08-21T17:54:25Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Log of Pull Requests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || Closed in favor or #7807&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29131</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29131"/>
		<updated>2026-08-18T07:42:51Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Conclusion */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29130</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29130"/>
		<updated>2026-08-18T07:41:28Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* r.proj */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
One of the many r.proj scenarios: EPSG:4326 to EPSG:3857, memory=300, method=nearest&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular used GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29129</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29129"/>
		<updated>2026-08-18T07:40:45Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* r.proj */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data more easily, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows as it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular used GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29128</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29128"/>
		<updated>2026-08-18T07:39:37Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Pytest and Benchmarks */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three modules' testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular used GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29127</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29127"/>
		<updated>2026-08-18T07:39:03Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Future work */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfers directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and need to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular used GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29126</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29126"/>
		<updated>2026-08-18T07:35:36Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Conclusion */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfer directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and needs to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
&lt;br /&gt;
Three of the more popular used GRASS raster modules now run on multiple cores instead of one, and they do it without changing a single value in the output. Users pick how many threads they want to use and how much memory to give them, and the module does the rest.&lt;br /&gt;
&lt;br /&gt;
Along the way I encountered some bugs and was able to fix them. Two of the more prominent bugs were data races in GRASS libraries. One of them sitting there since 2012, and a crash in r.geomorphon on small regions. All three modules now have pytest suites and published benchmarks, so anyone can check the numbers and build on the work.&lt;br /&gt;
&lt;br /&gt;
I want to thank my mentors Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29125</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29125"/>
		<updated>2026-08-18T07:33:05Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Future work */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still plenty left to do here:&lt;br /&gt;
&lt;br /&gt;
* Many raster modules still run single threaded, so they need to be parallelized. The approach used in these three modules transfer directly to any module that works row by row.&lt;br /&gt;
* The progress reporting function in the GRASS library is not thread safe, so parallel modules currently have to work around it. A library level fix would clean that up for every module at once.&lt;br /&gt;
* Several modules still use the old testsuite framework and needs to move towards the pytest format.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
In conclusion, I met all of my project’s goals. All three modules now:&lt;br /&gt;
run in parallel with output being correct bit for bit&lt;br /&gt;
Have pytests and published benchmarks so users can test the module&lt;br /&gt;
I also fixed any bugs I could across the three modules. &lt;br /&gt;
I want to thank Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29124</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29124"/>
		<updated>2026-08-18T07:15:56Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Future work */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still scope to lot that can be improved/added on including:&lt;br /&gt;
There are still many modules that run single threaded that can be parallelized.&lt;br /&gt;
The memory bounded approach used here transfers to other modules. Any raster module that works row by row can be parallelized the same way. &lt;br /&gt;
Many modules still need their testsuites migrated to pytest.&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
In conclusion, I met all of my project’s goals. All three modules now:&lt;br /&gt;
run in parallel with output being correct bit for bit&lt;br /&gt;
Have pytests and published benchmarks so users can test the module&lt;br /&gt;
I also fixed any bugs I could across the three modules. &lt;br /&gt;
I want to thank Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29123</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29123"/>
		<updated>2026-08-18T07:13:59Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* r.proj */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.27x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 2.17x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.54x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.19x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still scope to lot that can be improved/added on including:&lt;br /&gt;
There are still many modules that run single threaded that can be parallelized.&lt;br /&gt;
The memory bounded approach used here transfers to other modules. Any raster module that works row by row can be parallelized the same way. &lt;br /&gt;
Many modules still need their testsuites migrated to pytest.&lt;br /&gt;
&lt;br /&gt;
Conclusion&lt;br /&gt;
In conclusion, I met all of my project’s goals. All three modules now:&lt;br /&gt;
run in parallel with output being correct bit for bit&lt;br /&gt;
Have pytests and published benchmarks so users can test the module&lt;br /&gt;
I also fixed any bugs I could across the three modules. &lt;br /&gt;
I want to thank Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29122</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29122"/>
		<updated>2026-08-18T07:13:43Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* r.geomorphon */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.9x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.5x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 5.3x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still scope to lot that can be improved/added on including:&lt;br /&gt;
There are still many modules that run single threaded that can be parallelized.&lt;br /&gt;
The memory bounded approach used here transfers to other modules. Any raster module that works row by row can be parallelized the same way. &lt;br /&gt;
Many modules still need their testsuites migrated to pytest.&lt;br /&gt;
&lt;br /&gt;
Conclusion&lt;br /&gt;
In conclusion, I met all of my project’s goals. All three modules now:&lt;br /&gt;
run in parallel with output being correct bit for bit&lt;br /&gt;
Have pytests and published benchmarks so users can test the module&lt;br /&gt;
I also fixed any bugs I could across the three modules. &lt;br /&gt;
I want to thank Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29121</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29121"/>
		<updated>2026-08-18T07:13:08Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Final Report */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Number of threads !! Speedup&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 1.0x&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1.91x&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3.37x&lt;br /&gt;
|-&lt;br /&gt;
| 8 || 4.55x&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads. &lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
There is still scope to lot that can be improved/added on including:&lt;br /&gt;
There are still many modules that run single threaded that can be parallelized.&lt;br /&gt;
The memory bounded approach used here transfers to other modules. Any raster module that works row by row can be parallelized the same way. &lt;br /&gt;
Many modules still need their testsuites migrated to pytest.&lt;br /&gt;
&lt;br /&gt;
Conclusion&lt;br /&gt;
In conclusion, I met all of my project’s goals. All three modules now:&lt;br /&gt;
run in parallel with output being correct bit for bit&lt;br /&gt;
Have pytests and published benchmarks so users can test the module&lt;br /&gt;
I also fixed any bugs I could across the three modules. &lt;br /&gt;
I want to thank Anna Petrasova and Huidae Cho for their guidance and reviews throughout the summer, and the GRASS community for being welcoming the whole way.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29120</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29120"/>
		<updated>2026-08-18T06:58:13Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Pytest and Benchmarks = */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads. &lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
=== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29119</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29119"/>
		<updated>2026-08-18T06:57:56Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Final Report */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Final Report =&lt;br /&gt;
&lt;br /&gt;
== Abstract ==&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
== My Contributions ==&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads. &lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29118</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29118"/>
		<updated>2026-08-18T06:56:31Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Reports */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Final Report ==&lt;br /&gt;
&lt;br /&gt;
=== Abstract ===&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
=== My Contributions ===&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads. &lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29117</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29117"/>
		<updated>2026-08-18T06:55:28Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Final Report */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Final Report ==&lt;br /&gt;
&lt;br /&gt;
=== Abstract ===&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;br /&gt;
&lt;br /&gt;
=== My Contributions ===&lt;br /&gt;
=== r.param.scale ===&lt;br /&gt;
&lt;br /&gt;
r.param.scale is a module that takes an elevation map and computes terrain parameters like slope and curvature at every cell. For each cell in the input map, it looks at the cell’s neighbors and takes their height to find a curved shape that passes through them. The module finds the terrain parameters from this shape. &lt;br /&gt;
&lt;br /&gt;
Each cell's answer depends only on its neighbors, so the map can be split across threads and each thread can work through its share independently. This made the parallelization process simple, but the problem was optimizing memory. The one threaded version of the module loaded the whole map into RAM at once. That was fine for a small map and a single thread, but it doesn’t scale. I rewrote that logic to work through the map in horizontal bands, where each band is sized so it fits inside whatever memory the user allows. The threads then split the rows inside each band. This gives around 4.6x speedup compared to serial at eight threads.&lt;br /&gt;
&lt;br /&gt;
While testing the parallelization, I came across a bug that existed since 2012 within the core GRASS math library. There was a race condition between two shared variables that caused wrong answers to occur when multiple threads were called at the same time. I diagnosed the issue, found a solution, tested it, and added the bug fix within the same PR that parallelized r.param.scale.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== r.geomorphon ===&lt;br /&gt;
&lt;br /&gt;
r.geomorphon takes an elevation map as input and labels every cell as a landform type like ridge, valley, slope or flat. It does this by looking outward from each cell in eight different directions and checking if the ground rises or falls along each of the directions. The pattern of ups and downs helps decide what type of landform the cell is.&lt;br /&gt;
&lt;br /&gt;
Almost all of the run time was in that one step, and each cell's answer only depends on the terrain around it. So, I split the output rows across threads. To keep the memory usage limited to how much the user allows, the module now works through the map in horizontal bands. A horizontal band is a certain number of rows clumped together all handled by threads. The size of each band depends on how much memory the user allows. A cell near the top or bottom of a band still needs to see the cells just outside the band. This is because its landform depends on the terrain around it. So each band is read with a few extra rows above and below it. That way every cell sees exactly the same neighbors it would if the whole map were loaded. Not only does this allow the output to be correct, but it also gives around 5.2x speedup at eight threads. &lt;br /&gt;
&lt;br /&gt;
=== r.proj ===&lt;br /&gt;
&lt;br /&gt;
r.proj is a module that takes a map made in one coordinate system and converts it into another. This is important because many GIS projects need maps with the same projection so that they can take accurate measurements, process data easier, or align raster data. To build the new map, r.proj takes each cell of the new map, finds the spot in the old map that it corresponds to, and copies the value from there.&lt;br /&gt;
&lt;br /&gt;
Each output cell can be computed on its own, so the rows can be split across threads. So similar to the other 2 modules, the parallelization is simple, but memory is the problem again. Because the two maps are in different projections, one output row does not match one input row. It touches a curved band of input rows, and to compute a set of output rows you have to hold all the input rows they touch. That number changes with the projection and with where you are in the map. On top of that, you have to know where you land before you read the input.&lt;br /&gt;
&lt;br /&gt;
Instead of working this out band by band, the module figures it out once at the start. The module projects the edges of the output map into the input map. Then for each output row, and for each block of columns within that row, it records the input rows that block needs and stores it within a table. That table is used to look things up. After that, sizing a band properly is just looking up the values from the table. It takes as many output rows it can fit inside the memory the user allows, and if even one full row is too wide it cuts the row into column tiles. If nothing fits at all it uses the original serial code instead. Then each band's input rows are read once and the threads split up the output rows. This makes the speedup range from 2.5x to 5.7x depending on the map and the memory allowed.&lt;br /&gt;
&lt;br /&gt;
== Pytest and Benchmarks ===&lt;br /&gt;
I replaced all three module’s testsuite with a new pytest and added a benchmarking script within the module.&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29116</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29116"/>
		<updated>2026-08-18T06:44:27Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Final Report */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Final Report ==&lt;br /&gt;
&lt;br /&gt;
=== Abstract ===&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29115</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29115"/>
		<updated>2026-08-18T06:43:51Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Reports */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Final Report ==&lt;br /&gt;
&lt;br /&gt;
 = Abstract =&lt;br /&gt;
Many GRASS raster modules still do their work on a single core. On a laptop with eight cores that means seven of them sit idle, while only one core does all the work. On a large map, only using one thread means the wait time to receive the output will be long. This project parallelizes three commonly used modules so they can use multiple cores at once, cutting wait time significantly. The modules are r.param.scale, r.geomorphon and r.proj. Each of the three modules is parallelized using OpenMP and a memory bounded approach that gives users the freedom to choose how much memory they want to use. As a result of this project, users can now benefit from a speedup depending on the number of threads they allow and the amount of memory they allocate. The outputs are exactly the same as when the modules ran single threaded, bit for bit.&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29114</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29114"/>
		<updated>2026-08-18T06:42:43Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Log of Pull Requests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29113</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29113"/>
		<updated>2026-08-17T06:07:54Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 12 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29112</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29112"/>
		<updated>2026-08-17T06:07:08Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 12 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 12 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906/1&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29111</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29111"/>
		<updated>2026-08-17T06:06:50Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 12 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-12-report/154906/1&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29110</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29110"/>
		<updated>2026-08-14T06:48:33Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 11 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 11 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29109</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29109"/>
		<updated>2026-08-14T06:48:08Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 11 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
 https://discourse.osgeo.org/t/gsoc-coding-period-week-11-report/154833&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29108</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29108"/>
		<updated>2026-08-14T06:47:18Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 10 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 10 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-10-report/154761&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29107</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29107"/>
		<updated>2026-08-14T06:46:36Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 9 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 9 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-9-report/154659&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29106</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29106"/>
		<updated>2026-08-14T06:46:03Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 8 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 8 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-8-report/154560&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29105</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29105"/>
		<updated>2026-08-14T06:45:34Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 7 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 7 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-7-report/154481&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29104</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29104"/>
		<updated>2026-08-14T06:45:11Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 6 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29103</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29103"/>
		<updated>2026-08-14T06:45:01Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 6 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in week 6 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29102</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29102"/>
		<updated>2026-08-14T06:44:19Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 5 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
https://discourse.osgeo.org/t/gsoc-coding-period-week-6-report/154480&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29101</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29101"/>
		<updated>2026-08-14T06:43:42Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 4 &amp;amp; Week 5 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
Posted what I did in weeks 4 and 5 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29100</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29100"/>
		<updated>2026-08-14T06:43:13Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 4 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 &amp;amp; Week 5 ==&lt;br /&gt;
&lt;br /&gt;
https://discourse.osgeo.org/t/gsoc-coding-period-week-4-5-report/154479&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29099</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29099"/>
		<updated>2026-08-14T05:38:29Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 3 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
Posted what I did in week 3 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-3-report/154478&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29098</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29098"/>
		<updated>2026-08-14T05:19:21Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
Posted what I did in week 2 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-2-report/154036&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29097</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29097"/>
		<updated>2026-08-14T05:08:12Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
Posted what I did in week 1 in the OSGeo discourse: https://discourse.osgeo.org/t/gsoc-coding-period-week-1-report/153925&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29096</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29096"/>
		<updated>2026-08-14T03:45:42Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 9 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
What I worked on in week 1: I built and benchmarked two different ways to parallelize r.param.scale. The first version reads strips straight from the disk, conceptually very similar to how r.neighbors is parallelized (draft PR #7440), and the second loads them into RAM per thread (draft PR #7442). Both gave exactly the same output as the original serial code across window sizes 5 to 51. There was some speedup compared to the serial version, but not as fast as it should be. In Friday’s meeting the mentors and I agreed #7440 is conceptually the better route to take. I also benchmarked r.neighbors on my computer to make sure it wasn’t a reason I was seeing poor speedup ratios. It scaled well there at larger windows, so my code was the reason for the weaker scaling, not the machine. I ended with doing a much deeper analysis of what the differences were between my disk approach and r.neighbors. I found the main gaps to be not having the two-level band structure, each thread holding too much of the map in memory, no memory limit option, and no mask handling.&lt;br /&gt;
&lt;br /&gt;
What I’m doing this week: Refactoring #7440 to make sure I cover all the gaps I mentioned above. Once I’m done I will update the draft PR and also add in benchmarking scripts I used so mentors can try replicating results from their end if they want to.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me: Nothing, I should be good to go.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of the r.proj work this week was finishing the remaining review items and getting everything ready for review. I implemented the two speedup items from the earlier review, keeping the input strip resident across bands instead of re-reading it, and writing the previous band’s output while the next band computes. Both of them together improved the total speedup on every test case, with the biggest improvements being on the hard map cases since that’s where the serial phases were the problem. For testing, I made the method reference test move into their own PR #7766 based on review feedback, replacing the old testsuite with pytest. The main PR #7627 keeps new pytest cases that check the parallel output matches serial. I also made a separate small PR #7764 that fixed a data race on two globals in the projection library and it got merged this week. I also added a benchmark script for the module, ran the full benchmark grid, and posted the results and scaling graphs in the PR. With all that done I rewrote the PR description and marked the module ready for review.&lt;br /&gt;
&lt;br /&gt;
The rest of the week went to r.geomorphon, which moved faster than I expected. I first did a thorough recon of the module and it showed each output row only needs a fixed window of input rows sized by the search option. This is a very similar idea that r.param.scale and r.neighbors ran on. So I started working towards the parallelization, and along the way I found an already existing crash on regions smaller than the search window and fixed it as a small separate PR #7773.&lt;br /&gt;
&lt;br /&gt;
For the parallelization I first made two per cell globals into function parameters so threads would not fight over them, then rewrote the input handling into per band strips sized from a new memory option. I then added the parallel region with per thread file descriptors, and finished with a nprocs option. The hard part was reproducing the original code’s buffer positioning at map edges exactly.&lt;br /&gt;
&lt;br /&gt;
I ran benchmarks for the parallel r.geomorphon on my machine and it has a promising speedup. It got about 1.9x at 2 threads, 3.3 to 3.7x at 4, and 4.2 to 5.1x at 8 versus serial. The best speedups were on lat-lon maps since they are the most compute heavy. I wrote the benchmark script for it as well and pushed it on PR #7783.&lt;br /&gt;
&lt;br /&gt;
What I’m doing in week 10:&lt;br /&gt;
&lt;br /&gt;
I’m making a pytest for the parallel correctness and then doing a full review pass over the diff. For r.proj, I’ll be responding to the review and any concerns that come up.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me? Nothing, working on wrapping up r.geomorphon while mentors take a look at r.proj.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29095</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29095"/>
		<updated>2026-08-14T03:33:28Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Week 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
What I worked on in week 1: I built and benchmarked two different ways to parallelize r.param.scale. The first version reads strips straight from the disk, conceptually very similar to how r.neighbors is parallelized (draft PR #7440), and the second loads them into RAM per thread (draft PR #7442). Both gave exactly the same output as the original serial code across window sizes 5 to 51. There was some speedup compared to the serial version, but not as fast as it should be. In Friday’s meeting the mentors and I agreed #7440 is conceptually the better route to take. I also benchmarked r.neighbors on my computer to make sure it wasn’t a reason I was seeing poor speedup ratios. It scaled well there at larger windows, so my code was the reason for the weaker scaling, not the machine. I ended with doing a much deeper analysis of what the differences were between my disk approach and r.neighbors. I found the main gaps to be not having the two-level band structure, each thread holding too much of the map in memory, no memory limit option, and no mask handling.&lt;br /&gt;
&lt;br /&gt;
What I’m doing this week: Refactoring #7440 to make sure I cover all the gaps I mentioned above. Once I’m done I will update the draft PR and also add in benchmarking scripts I used so mentors can try replicating results from their end if they want to.&lt;br /&gt;
&lt;br /&gt;
What’s blocking me: Nothing, I should be good to go.&lt;br /&gt;
&lt;br /&gt;
Thanks,&lt;br /&gt;
&lt;br /&gt;
Kaushik&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29094</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29094"/>
		<updated>2026-08-14T03:23:09Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Community Bonding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
# Started off with an introductory call with my mentors. &lt;br /&gt;
# I introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. &lt;br /&gt;
# I started this wiki page, created the abstract, project scope, and the timeline. &lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29093</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29093"/>
		<updated>2026-08-14T03:20:22Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Community Bonding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Started off with an introductory call with my mentors. I then introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the OSGeo discourse]. Then I started this wiki page, created the abstract, goal, and the timeline. &lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29092</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29092"/>
		<updated>2026-08-14T03:15:04Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Coding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Week 1 ==&lt;br /&gt;
&lt;br /&gt;
== Week 2 ==&lt;br /&gt;
&lt;br /&gt;
== Week 3 ==&lt;br /&gt;
&lt;br /&gt;
== Week 4 ==&lt;br /&gt;
&lt;br /&gt;
== Week 5 ==&lt;br /&gt;
&lt;br /&gt;
== Week 6 ==&lt;br /&gt;
&lt;br /&gt;
== Week 7 ==&lt;br /&gt;
&lt;br /&gt;
== Week 8 ==&lt;br /&gt;
&lt;br /&gt;
== Week 9 ==&lt;br /&gt;
&lt;br /&gt;
== Week 10 ==&lt;br /&gt;
&lt;br /&gt;
== Week 11 ==&lt;br /&gt;
&lt;br /&gt;
== Week 12 ==&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29091</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29091"/>
		<updated>2026-08-14T02:41:32Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Coding Period = */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Coding Period ==&lt;br /&gt;
&lt;br /&gt;
===Week 1===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29090</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29090"/>
		<updated>2026-08-14T02:40:45Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Reports */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
= Reports =&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Coding Period ===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29089</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29089"/>
		<updated>2026-08-14T02:40:28Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Reports */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Reports ====&lt;br /&gt;
&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Coding Period ===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29088</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29088"/>
		<updated>2026-08-14T02:40:13Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Coding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Reports ==&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
== Coding Period ===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29087</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29087"/>
		<updated>2026-08-14T02:38:28Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Community Bonding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Reports ==&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [https://discourse.osgeo.org/t/gsoc-2026-introduction/153672 the discourse].&lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
=== Coding Period ===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
	<entry>
		<id>https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29086</id>
		<title>GRASS GSoC 2026 Parallelizing r.proj and Raster Processing Modules in GRASS</title>
		<link rel="alternate" type="text/html" href="https://grasswiki.osgeo.org/w/index.php?title=GRASS_GSoC_2026_Parallelizing_r.proj_and_Raster_Processing_Modules_in_GRASS&amp;diff=29086"/>
		<updated>2026-08-14T02:35:13Z</updated>

		<summary type="html">&lt;p&gt;Kaushikraja: /* Community Bonding Period */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
| '''Student Name''' || Kaushik Raja&lt;br /&gt;
|-&lt;br /&gt;
| '''Organization''' || [https://numfocus.org/ NumFOCUS]&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor Name''' || Huidae Cho, Anna Petrasova, Vaclav Petras&lt;br /&gt;
|-&lt;br /&gt;
| '''GitHub Fork''' || [https://github.com/krcoder123/grass View Repo]&lt;br /&gt;
|-&lt;br /&gt;
| '''LinkedIn Profile''' || [https://www.linkedin.com/in/kaushikrraja/ View LinkedIn]&lt;br /&gt;
|}&lt;br /&gt;
== Abstract ==&lt;br /&gt;
R.proj, r.param.scale, and r.geomorphon are some of the most commonly used modules in GRASS. R.proj reprojects raster maps between coordinate systems. This is important because real world data comes in many different projections, and maps have to be in the same one before they can be analyzed together. So many GRASS workflows that combine data sources start with r.proj. R.param.scale calculates terrain parameters like slope and curvature by sliding a window over an elevation map. R.geomorphon classifies every cell of an elevation map into a landform like a ridge, valley, or peak. It does this by looking outward from each cell in eight different directions and checks whether the terrain rises above or drops below the line of sight. That pattern of visible horizons tells it what shape the land around the cell is.&lt;br /&gt;
&lt;br /&gt;
The problem with all three modules is that they have always been single threaded. On modern hardware most CPU cores sit idle while one core does all the work, so large maps take much longer time to work and return an output. The goal of this project is to parallelize them with OpenMP so that many threads work on the map at the same time.&lt;br /&gt;
&lt;br /&gt;
The main challenge with parallelizing these modules is memory. To help manage memory and get the most speedup ratios possible, each module will work on a chunk of rows at a time, called a band. The band is sized so that the input it needs stays under a memory limit the user controls. The threads then split the rows of the band among themselves and work on them at the same time. In r.proj, each thread also gets its own file descriptor and its own PROJ object (for thread safety). Some projections bend so much that even one full row’s width is too much input to fit under the memory limit, so those are read in smaller column pieces instead. In r.param.scale the old sliding window was replaced with this same band design, and in between I found and fixed a data race in the GRASS math library. &lt;br /&gt;
&lt;br /&gt;
As a result of this, users can use one of the three modules and get their outputs faster than before. Also, the band pattern used in these modules gives future contributors a template for parallelizing other raster modules.&lt;br /&gt;
&lt;br /&gt;
== Project Scope ==&lt;br /&gt;
# Parallelize r.param.scale with OpenMP under a user controlled memory limit&lt;br /&gt;
# Parallelize r.proj &lt;br /&gt;
# Parallelize r.geomorphon&lt;br /&gt;
# Add nprocs option so user can say how many threads form their machine they want to use&lt;br /&gt;
# Find and fix data races in the GRASS libraries that block modules from being parallelized&lt;br /&gt;
# Replace the old testsuites for r.param.scale, r.proj, and r.geomorphon with pytest tests&lt;br /&gt;
# Verify every parallel module produces output identical to the serial version&lt;br /&gt;
# Add a benchmarking script for each module and benchmark all modules across thread counts&lt;br /&gt;
&lt;br /&gt;
== Timeline ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Period !! Timeline !! Tasks !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Community Bonding Period&lt;br /&gt;
| May 1 - May 25&lt;br /&gt;
|&lt;br /&gt;
# Thread safety audit of gprojects library calls&lt;br /&gt;
# Study how the code in readcell.c works&lt;br /&gt;
# Set up benchmarking infrastructure and figure out how that works&lt;br /&gt;
# Look at remaining modules and understand how they work&lt;br /&gt;
# Finalize dev environment&lt;br /&gt;
# Confirm benchmarks are reproducible&lt;br /&gt;
# Agree on implementation details with mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | Official Coding Period&lt;br /&gt;
| May 25 - June 8&lt;br /&gt;
|&lt;br /&gt;
# Build r.proj proof of concept with RAM buffer and per thread PROJ contexts (PR #7185)&lt;br /&gt;
# Benchmark RAM buffer approach against tile cache approach&lt;br /&gt;
# Add user controlled memory option&lt;br /&gt;
# Build first r.param.scale parallel draft (PR #7236)&lt;br /&gt;
# Rule out a suspected Mac OpenMP bug, traced to benchmark setup&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 9 - June 22&lt;br /&gt;
|&lt;br /&gt;
# Redesign r.param.scale from a single strip into a two level band and chunk structure that honors the memory option, modeled on r.neighbors (PR #7440)&lt;br /&gt;
# Give each thread its own input access so threads no longer share one reader&lt;br /&gt;
# Measure 4.6x speedup at 8 threads, up from 2.7x before the redesign, with output identical to the serial module&lt;br /&gt;
# Trace a scaling dip to thread load imbalance using per thread timing, correlation of 0.99 between imbalance and slow runs, ruling out a CPU frequency explanation&lt;br /&gt;
# Find a data race in G_ludcmp in the GRASS math library affecting every parallel caller, prove it with ThreadSanitizer and a controlled toggle experiment, report it (issue #7539)&lt;br /&gt;
# Fix the race and verify with ThreadSanitizer showing 14 race reports before and 0 after, repeated test runs going from 10 failures in 100 to 0 in 20, and a 40 configuration bit identical output comparison&lt;br /&gt;
# Check every caller of the fixed function at runtime to confirm safety for dependent modules&lt;br /&gt;
# Debug CI failures across platforms including CMake PROJ linkage and unguarded OpenMP timer calls, resolve a git branch divergence, unskip and pass the pytest suites on Python 3.10 and 3.13, pass all 26 checks, mark PR #7440 ready for review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| June 23 - July 6&lt;br /&gt;
|&lt;br /&gt;
# Design and build the band based r.proj (PR #7627)&lt;br /&gt;
# Size each band by projecting its edges back to the input to find the rows it needs&lt;br /&gt;
# Cut peak memory from 763 MB to 130 MB with identical output&lt;br /&gt;
# Measure 2.9x total and 5.2x compute speedup at 8 threads on a 105 million cell map&lt;br /&gt;
# Answer two rounds of mentor review&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;background:#ffdead;&amp;quot; | July 7 - July 11&lt;br /&gt;
|&lt;br /&gt;
# Submit midterm evaluation&lt;br /&gt;
# Fix two CI failures, a missing PROJ dependency in the CMake build and OpenMP timer calls breaking the no OpenMP build&lt;br /&gt;
# Parallelize input reading with per thread file descriptors, 1.9x faster reads, about 3.0x total&lt;br /&gt;
# Move per thread PROJ context handling into the gproj library as new API functions at maintainer request&lt;br /&gt;
# Measure which projections need column splitting with a standalone footprint tool&lt;br /&gt;
# Split bands into column sections so tilted reprojections that failed under the memory cap now complete, verified identical on three projections&lt;br /&gt;
# Cut the section sizing search from 54 seconds to 2.6 seconds&lt;br /&gt;
# Benchmark everything and present results to mentors&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| July 20 - August 3&lt;br /&gt;
|&lt;br /&gt;
# Implement the two speedup items from review on PR #7627, keeping the input strip resident across bands and writing the previous band's output while the next band computes&lt;br /&gt;
# Run bilinear, bicubic and lanczos through the banded parallel path&lt;br /&gt;
# Move the method reference tests into their own PR #7766, replacing the old testsuite with pytest&lt;br /&gt;
# Fix a data race on two globals in the projection library (PR #7764, merged)&lt;br /&gt;
# Run the full benchmark grid, post results and scaling graphs, mark PR #7627 ready for review&lt;br /&gt;
# Look at r.geomorphon and fix an already existing crash on regions smaller than the search window (PR #7773, merged)&lt;br /&gt;
# Parallelize r.geomorphon with per band strips, per thread file descriptors and a nprocs option (PR #7783), about 5.1x at 8 threads&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 4 - August 11&lt;br /&gt;
|&lt;br /&gt;
# Rework the r.proj band sizing after review feedback that the search approach was hard to follow, replacing it with a footprint grid computed once when the user runs the module&lt;br /&gt;
# Fix a problem related to low memory slowdown on strongly curved projections and fix it by choosing band height and tile width together from the grid&lt;br /&gt;
# Verify that the output is correct across all methods, thread counts and memory settings, run the full benchmark matrix, open draft PR #7807&lt;br /&gt;
# Rebuild the r.geomorphon pytest suite on a DEM that produces all ten landform classes and address review (PR #7785)&lt;br /&gt;
# Address review on PR #7783&lt;br /&gt;
| Done&lt;br /&gt;
|-&lt;br /&gt;
| August 12 - August 18&lt;br /&gt;
|&lt;br /&gt;
# Address remaining review on PR #7785, merged&lt;br /&gt;
# Rebase PR #7783 over main and add the parallel identity tests&lt;br /&gt;
# Address any review or questions on #7807 and #7766&lt;br /&gt;
# Update the wiki page with the final report&lt;br /&gt;
|In Progress&lt;br /&gt;
|-&lt;br /&gt;
| Final Week&lt;br /&gt;
| August 19 - August 26&lt;br /&gt;
|&lt;br /&gt;
# Finish the final report and share it with mentors&lt;br /&gt;
# Submit the final work product and final evaluation&lt;br /&gt;
# Respond to review on the open PRs&lt;br /&gt;
| In Progress&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Reports ==&lt;br /&gt;
== Community Bonding Period ==&lt;br /&gt;
&lt;br /&gt;
Had an introductory call with my mentors and introduced myself to the community on [the discourse][https://discourse.osgeo.org/t/gsoc-2026-introduction/153672]. &lt;br /&gt;
&lt;br /&gt;
During this period I:&lt;br /&gt;
&lt;br /&gt;
# Did a thread safety audit of the gproj library calls&lt;br /&gt;
# Studied how the code in readcell.c works&lt;br /&gt;
# Set up the benchmarking infrastructure and figured out how it works&lt;br /&gt;
# Looked at the remaining modules and understood how they work&lt;br /&gt;
# Finalized my dev environment&lt;br /&gt;
# Confirmed my benchmarks are reproducible&lt;br /&gt;
# Decided to parallelize r.param.scale first, r.proj second, and then finally r.geomorphon&lt;br /&gt;
# Worked on identifying methods to parallelize r.param.scale&lt;br /&gt;
&lt;br /&gt;
=== Coding Period ===&lt;br /&gt;
&lt;br /&gt;
== Log of Pull Requests ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pull Request / Issue !! Description !! Status&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7440 PR #7440] || r.param.scale parallelization and G_ludcmp race fix || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7764 PR #7764] || Fix a data race on two globals in the projection library || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7773 PR #7773] || Fix a r.geomorphon crash on regions smaller than the search window || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7785 PR #7785] || Replace the r.geomorphon testsuite with pytest tests || Merged&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7627 PR #7627] || r.proj parallelization with memory bounded bands || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7807 PR #7807] || Simpler r.proj band sizing from a precomputed footprint grid || Draft, in review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7783 PR #7783] || r.geomorphon parallelization || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7766 PR #7766] || Replace the r.proj method testsuite with pytest tests || In review&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/issues/7539 Issue #7539] || G_ludcmp data race report || Addressed within #7440&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7185 PR #7185] || r.proj proof of concept || Closed in favor of #7627&lt;br /&gt;
|-&lt;br /&gt;
| [https://github.com/OSGeo/grass/pull/7236 PR #7236] || r.param.scale proof of concept || Closed in favor of #7440&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Kaushikraja</name></author>
	</entry>
</feed>