Skip to content

Keeping memory bounded

A 40 GiB input tiles on a 54 GiB machine because tylertoo never holds the whole dataset. In the Brazil demo, convert peaked at 9.6 GiB while reading 40.7 GiB over the network, and export held to 1.56 GiB while streaming all fifteen levels. This topic explains the streaming model those numbers come from, the two-pass structure behind it, and the knobs that trade memory against speed when a file outgrows RAM or a run gets tight.

Design decisions

Peak memory tracks the largest row group. The streaming reader holds one read batch plus a set of compact per-feature tables, never the decoded dataset. Memory therefore scales with the largest row group and the feature count, not the file size, which is why a multi-gigabyte input tiles in single-digit gigabytes. It is also why row-group sizing during input preparation is a memory decision, not just a throughput one.

Two passes avoid holding the whole dataset. Pass 1 scans the input to assign each feature its levels and apply the density budget, keeping only bounding boxes, geometry kinds, and sort keys. Pass 2 re-reads the input per level and simplifies and writes it batch by batch. The cost is reading the input twice. The benefit is a peak of O(read batch + winner tables) instead of O(dataset), which on a 632k-polygon file is the difference between well under 1 GB and several.

The auto profile spills output under memory pressure. Pass 2 accumulates each output level's rows before writing them. The speed profile keeps that buffer in RAM, bounded spills it to temporary Arrow IPC files, and auto estimates the buffer from feature and level counts and spills when it would exceed a fraction of available RAM. The output is byte-identical across all three, so the choice is purely about the memory ceiling.

Remote input stages to a local spill file. A remote convert fetches each column chunk it touches into a temporary file, growing to roughly one times the touched bytes. Later passes then re-read from local disk instead of the network. This bounds a remote run to about a single download of the data it needs, rather than one download per pass.

Export waves trade cores for memory. Export processes partitions in waves, holding one wave resident at a time. A wider wave keeps more cores busy at proportionally more peak memory. The default preflights a budget from the core count and available RAM, so the common case needs no tuning.

API walkthrough

Streaming instead of loading the dataset

The two-pass default. No flag turns it on; it is how convert runs. Pass 1 builds the winner tables, pass 2 writes the levels. This is the mechanism that delivers the O(row group) memory bound.

--no-streaming. Reverts to the in-memory pipeline, which decodes the whole dataset once. It can be marginally faster on small inputs that fit in RAM comfortably, at the cost of the memory bound. Reach for it only when the input is small and speed matters more than the ceiling.

--read-batch-size <rows>. The Arrow batch size for both passes, defaulting to 8192. Larger batches amortize per-batch overhead for a little more speed at proportionally more peak memory; smaller batches bound memory tighter. The default keeps per-batch transients in the tens of megabytes even for vertex-heavy polygons.

Choosing a memory profile

--profile auto|speed|bounded. Selects how pass 2 handles buffered output, per the profile decision above. auto is the default and the safe choice for large duplicating runs, which it steers toward bounded rather than risking an out-of-memory kill.

TYLERTOO_AUTO_MEM_LIMIT_BYTES. Overrides the available-RAM figure that auto reads when it cannot probe the machine, or when you want to reserve headroom for other work on the box.

Overlapping read and compute

--in-flight-batches N|auto. How many read batches move through the pass-2 pipeline at once. auto sizes this to the core count, clamped to 4 through 16. Raising it improves core utilization on long-pole geometries at proportionally more peak memory, since each in-flight batch stays resident. The chosen depth prints at pass-2 start.

Placing spill files

--spill-dir <path>. Where the remote-input stage file lands, and on tiles, where the intermediate overview lands. Point it at a volume with room for roughly the touched input size. A free-space preflight warns about a projected shortfall. The directory must exist. Local inputs never spill.

$TMPDIR. The default spill location when --spill-dir is unset. The caveat is that a small or full $TMPDIR volume turns a remote convert's stage write into a failure, so on a big remote run, set --spill-dir to somewhere you know has space.

Sizing export waves

--partition-wave N|auto. The number of partitions export holds resident per band. auto preflights a memory budget from the core count and available RAM. Override it with an explicit integer to cap memory harder on a shared machine, or to push utilization on a dedicated one. The output is byte-identical for every value.