Skip to content

Treevox: never let a voxelization batch span two chunk stripes - #731

Merged
amarcozzi merged 1 commit into
mainfrom
729-treevox-batch-stripes
Oct 16, 2026
Merged

amarcozzi merged 1 commit into
mainfrom
729-treevox-batch-stripes

Conversation

@amarcozzi

Copy link
Copy Markdown
Contributor

Closes #729.

Treevox orders chunks in 2x2 blocks and cuts the list into batches of num_workers. Each batch is read and written as one bounding box. With an odd number of chunk columns, the last block of each two-row stripe has only two chunks, so the next batch wrapped into the following stripe (e.g. (0,10), (1,10), (2,0), (2,1)), and its bounding box covered the full grid width:

chunk columns worst batch vs. a 2x2 block
3 2.2×
7 5.2×
11 7.7× (1.58 GB vs 0.20 GB per band at 50 z)
15 11.1×

All four treevox OOM kills in the last 30 days (2026-10-10 ×3, 2026-10-15 ×1) were 6×11-chunk grids that died in batch 6/17, the wrapping batch.

Changes

  • voxelize.chunk_batches cuts the block-ordered chunk list into batches of up to batch_size, ending a batch at every stripe (row // 2) boundary. With 4 workers, every batch is now one 2x2 block or, at the end of a stripe, one 2x1 block. The 2× batches that followed the wrap are gone too.
  • _run_voxelization_batches iterates those batches. The number of batches can rise slightly (17 → 18 for 6×11).

Chunk order is unchanged, so output is unchanged: additive bands sum the same contributions, and overwrite bands still resolve last-writer-wins in chunk order whether two chunks share a batch or not. (Within a batch, a worker whose write equals the pre-batch value doesn't overwrite. That edge case is already documented on masked_merge.)

Tests

tests/test_voxelize.py::TestChunkBatches uses the prod shape (6×11 chunks, 2674×5224 cells, chunk_xy 500). It checks that the batches keep chunk order and coverage, that no batch spans two stripes, and that no bounding box exceeds one 2x2 block plus halo. It also checks that even column counts match the old slicing, plus a single-row grid and an empty list. tests/test_voxelize.py and tests/handlers/test_voxelize.py pass: 256 tests.

Not verified on Cloud Run: the memory reduction follows from the bounding-box sizes and was not measured in a deployed run.

Chunks are ordered in 2x2 blocks and cut into batches of num_workers.
With an odd number of chunk columns the last block of each stripe has
two chunks, so the next batch wrapped into the following stripe and its
union read covered the full grid width (7.7x a normal batch at 11
columns). That batch OOM-killed treevox at 16 GiB on ~700M-voxel grids.
End each batch at a stripe boundary; chunk order is unchanged.
@amarcozzi amarcozzi self-assigned this Oct 16, 2026
@amarcozzi
amarcozzi merged commit 71cd499 into main Oct 16, 2026
1 check passed
@amarcozzi

Copy link
Copy Markdown
Contributor Author

Verified on Cloud Run, plus one correction to the description.

Setup: the 2026-10-15 crashed job, re-run on a temporary private service at prod size (4 vCPU / 16 GiB, 1 instance). Same 251,198-tree inventory, 6×11 chunks, 50×2674×5224 at 1 m, with the user's bands (bulk_density.foliage.live, fuel_moisture.live, leaf_area_density, savr.foliage). Run first on the pre-fix image, then on the merged one.

pre-fix this PR
batches 17 18
batch 6 read / write 3.81 s / 18.28 s 0.5 s / normal
slowest read / write in the job 3.81 s / 18.28 s (batch 6) 1.55 s / 8.03 s
total 294 s 297 s

The pre-fix run survived this time (2 of 3 identical jobs survived on 10-10). Cloud Run's per-minute memory metric (p99 42% vs 32%) is too coarse to show the seconds-long spike. The oversized batch is gone.

Correction: the description says output is unchanged; that's true only up to float32 rounding. With source.seed fixed, the two images' outputs match byte for byte in savr.foliage and fuel_moisture.live and in every chunk outside chunk rows 2–3. Of 264 chunks, 28 differ, all in the additive bands (bulk_density.foliage.live, leaf_area_density) and only in rows 2–3, the rows whose batch grouping changed. Each differs in 1–18 of 12.5M cells, by at most 1.6e-7 relative (about one float32 rounding step). The cause is a different summation order where halos overlap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Treevox OOM: a batch that wraps to the next chunk row reads the full grid width

1 participant