feat(raster): byte-bounded batches for RS_EnsureLoaded (EnsureLoadedExec + sedona.raster.max_batch_bytes) - #1252
Draft
james-willis wants to merge 3 commits into
Draft
Conversation
Add `sedona_raster::size`: `estimated_band_bytes`, `estimated_raster_bytes` and `estimated_row_bytes` compute Σ_bands Π raw_source_shape × pixel bytes from band metadata alone. For an InDb band that equals the buffer length; for an OutDb band it is the size loading it will allocate — which is what a memory budget needs to know before anything is loaded. The raw source shape (not the visible shape) is used so a broadcast view doesn't inflate the figure and a slice doesn't shrink it; bands sharing one buffer are each counted in full, the safe direction for a budget. Refs apache#1220
New `raster` namespace on `SedonaOptions` with `max_batch_bytes` (default 256 MiB): the byte budget RS_EnsureLoaded materializes per batch, sized from the metadata-only raster estimate. `0` disables the slicing. Consumed by the EnsureLoadedExec in the following commit. Refs apache#1220
james-willis
force-pushed
the
jw/raster-batch-budget
branch
from
September 10, 2026 20:40
33d3856 to
c233ae7
Compare
…d raster bytes `datafusion.execution.batch_size` counts rows, but a row holding a raster can carry megabytes of pixels, so the batch DataFusion hands RS_EnsureLoaded can be gigabytes once loaded (8192 × 1 MiB spatialbench tiles = 8 GiB per partition). Nothing upstream can shrink it: DataFusion's AsyncFuncExec re-coalesces its input to exactly `batch_size` rows before evaluating, and the UDF's `ideal_batch_size` only chunks the invocation — the results are concatenated back into one array. Add `RasterBatchBudgetRule`, a physical optimizer rule appended after DataFusion's own, that replaces every AsyncFuncExec carrying an `rs_ensureloaded` call with `EnsureLoadedExec`: the same expressions, schema and plan properties (including the `__async_fn_N` output columns), but each input batch is sliced so the metadata-only byte estimate of the rasters about to be materialized stays within `sedona.raster.max_batch_bytes`, and each slice is evaluated and emitted as its own batch. A row over budget goes alone, null rows cost nothing, empty batches flow through, and other async expressions in the same node are evaluated per slice. `SedonaContext` installs the rule alongside the existing planner rules and, when a memory limit is configured, lowers the budget to 1/8 of the per-partition limit (floor 16 MiB), mirroring the spill threshold. Streaming 2048 one-MiB OutDb rasters through `SELECT RS_EnsureLoaded(rast)` (mock loader, single partition, debug build) peaks at 2.20 GB RSS unbounded versus 779 MB at the 256 MiB default, 591 MB at 64 MiB and 145 MB at 16 MiB. Refs apache#1220
james-willis
force-pushed
the
jw/raster-batch-budget
branch
from
September 10, 2026 21:47
c233ae7 to
854f13f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step of the plan on #1220 (Linear DB-273): bound the bytes
RS_EnsureLoadedmaterializes per batch, sized dynamically per input batch from metadata, inside a Sedona-owned operator.Problem
datafusion.execution.batch_sizecounts rows, but a row holding a raster can carry megabytes of pixels, so the batch DataFusion handsRS_EnsureLoadedcan be gigabytes once loaded — 8192 × 1 MiB spatialbench tiles is 8 GiB per partition, which is the sf1 OOM. The obvious knobs don't help: DataFusion'sAsyncFuncExecre-coalesces its input to exactlybatch_sizerows before evaluating, and the UDF'sideal_batch_sizeonly chunks the invocation — the per-chunk results areconcated back into one array. Same in DataFusion 54.1.What this does
sedona_raster::size— metadata-only estimates:Σ_bands Π raw_source_shape × pixel bytes. One formula for both storage kinds: for InDb bands it equals the buffer length, for OutDb bands it is the post-load size. Uses the raw source shape (not the visible one) so views don't distort it; shared buffers over-count, the safe direction. Never touchesdata, so it works on OutDb rows before anything is loaded.sedona.raster.max_batch_bytes— newrasternamespace onSedonaOptions,SET-able. Default 256 MiB; when a memory limit is configured,SedonaContextlowers it to 1/8 of the per-partition limit (floor 16 MiB), mirroring how the spill threshold is derived.0disables.RasterBatchBudgetRule+EnsureLoadedExec(sedona-query-planner) — a physical optimizer rule appended after DataFusion's own replaces everyAsyncFuncExeccarryingrs_ensureloadedwithEnsureLoadedExec: same expressions, same schema and plan properties (including the__async_fn_Noutput columns the planner projects on top), but each input batch is sliced so the estimated bytes about to be materialized stay within the budget, and each slice is evaluated and emitted as its own batch. A row over budget goes alone; null rows cost nothing; empty batches flow through; other async expressions in the same node are evaluated per slice. Slices are zero-copyRecordBatch::slices, so order is preserved.SedonaContext::new_local_interactive_with_runtime_envnext to the existing planner rules (SedonaContext::new()wraps an externalSessionContextand, as today, installs no rules).Rebased on
mainwith #1251 merged and DataFusion 54.1: each slice now reaches the loader as one bundledload()per loader, which the end-to-end test asserts.Measurements
2048 OutDb rasters of 1024 × 1024 UInt8 (1 MiB each) through
SELECT RS_EnsureLoaded(rast), mock loader returning committed pages, single partition, output streamed and dropped, debug build, macOS max RSS of the test process:sedona.raster.max_batch_bytesRSS is a high-water mark that includes allocator retention, so it tracks the budget loosely rather than matching it; the point is that peak memory now follows the budget instead of the row count.
Not in this PR (later steps of the same plan)
FilterExec/CoalesceBatchesExec/RepartitionExecfrom re-batching downstream. The end-to-end test pinstarget_partitions = 1so the round-robin repartition the planner puts above the exec doesn't obscure its output.RS_FromGDALRaster,RS_AsRaster, upsampling).Testing
sedona-raster: estimator tests — InDb equals buffer length, OutDb from metadata without loading, raw vs visible shape under a view, multi-band + null + zero-band rows, overflow rejected instead of wrapping.sedona-query-planner:slice_rangesunit test; exec tests with a mockrs_ensureloadedasync UDF — uniform rasters sliced to the budget, oversized rows alone with nulls free,0disables, output schema and__async_fn_0column parity withAsyncFuncExec, empty batch, the rule ignores unrelated async UDFs, mixed async expressions sized by the ensure-loaded argument only.sedona: end-to-end SQL throughSedonaContextwith a mock loader andSET sedona.raster.max_batch_bytes = 600:EXPLAINshowsEnsureLoadedExecand noAsyncFuncExec; output batches are[2, 2, 2]and the loader receives exactly one bundled call per slice ([2, 2, 2]requests).sedona-raster195,sedona-common10,sedona-query-planner90,sedona101 (3 skipped for missing geoarrow-data assets). clippy and fmt clean.