Repository navigation
perf: 3.4x faster planet build (2h10 -> 39 min), less RAM, deterministic output - #939
Conversation
|
Very neat set of changes, thank you. I observed less of a speedup but then my machine is rather less powerful and fairly IO-constrained: the UK (RAM-resident) went from 4m33 to 3m14, Europe (RAM-resident) went from 1hr11 to 58m, the planet (with Could you take out the code guarded by @laem @etienneJr If you have time to check out this PR I'd be really interested to hear your experiences - it looks like it could be a handy speedup for cartes.app. |
|
These two PR are great ! We get slightly faster tiles to transfer with MLT, faster decoding, and faster generation with less RAM. This is especially cool because RAM is on shortage. Our cloud provider (Scaleway) has a shortage of 384 Gb or RAM compute machines. Will test as soon as possible, thanks for the mention. |
29f0132 to
9f48efd
Compare
|
Thanks for testing, and for the numbers!
On the smaller speedup: my numbers were in-memory runs on a 96-thread EPYC, where the work is CPU-bound. Most of these changes cut CPU work, so on an IO-constrained machine, and especially with I also corrected the output section of the description: the geometry differences from the deterministic clipping are a bit larger than I first wrote (details there). They are the same kind of difference the original already produced between runs. |
|
Thank you - this is an amazing speedup. I'm still looking through the changes in detail, but I'm confident enough in what I've seen in outline that I'm happy to merge now and we can push any further adjustments if needed. The ability to run tilemaker on the planet in less than an hour is pretty impressive! |
Yes, that's great. If only we had daily osm.pbf releases :/ To my knownledge, they're only weekly, even with torrents. |
Gain: callgrind on Luxembourg --store drops from 125.7 to 121.7 G instructions (-3.2%); output identical (all 273,080 Germany tiles). ShardedNodeStore didn't override the batched at(ids, n, out) from systemed#939, so --store looked up every node of every way on its own plus a contains() check, the top entry in a --store profile. Now the whole batch is looked up in the shard of its first node, with per-node lookups only for ways crossing a shard boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…re output now matches in-memory) With shards, a relation was read only in the pass for the shard of its first member way; if that way wasn't in the extract, every pass skipped it. Germany --store vs in-memory: 119 missing tiles and ~5,700 missing features (Szczecin Lagoon, Usedom, border nature reserves) -> 0 differences in 273,199 tiles. Decide the shard by the first member way that is in the store; relations with no stored member ways are read in the first pass. Bug predates systemed#939. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Large clipped polygons such as coastline/ocean shapefiles are written to tiles invalid: scaling them onto the integer tile grid introduces self-intersections. Until now only SIMPLIFIED geometry was repaired, so those layers stayed invalid and can be mis-rendered by strict renderers - as missing water, or as land hidden behind an ocean polygon. New layer option invalid_polygon_repair_scope: "simplified_only" (default, unchanged behaviour) repairs only invalid polygons that simplification may have broken; "all" is a superset and additionally repairs polygons that were merely clipped and quantised. For those, repair_multi_polygon runs with a strict area guard that also rejects any repair GROWING a polygon - growing means a hole was filled, which would make an island disappear. That bound must not apply to simplified geometry, where legitimate repairs do change area; applying it globally measurably made things worse (5 -> 20 invalid features in one tile). The bound is passed as a scoped enum, AreaBound, rather than a bool: other changes also append parameters to repair_multi_polygon (systemed#939 a bool, systemed#949 a bool*), and with a bool here a merge that puts the parameters in the wrong order would still compile, silently. Verified: with the default the output is unchanged (Bangladesh z7 96/55: 166 features, 5 invalid, 319 holes). With "all" on the ocean layers, that tile goes to 4 invalid / 318 holes and the ocean feature behind a reported rendering artefact becomes valid; Denmark z5 16/10 improves 5 -> 1 invalid while Funen correctly stays land; Philippines z5 26/14 improves 7 -> 1 invalid with no land wrongly covered by ocean.
…ht, 67x less disk writing, 3.2% less CPU (#943) * fix(store): 12x faster --store under memory pressure, 67x less disk writes Sorting output objects scattered writes into the mmap'd store files, so the kernel rewrote the same dirty pages over and over. Germany with an 8 GB limit: 69+ min (1.3 TB written) -> 5:36 (20 GB written), identical tile content. Scatter into the existing heap buffer and copy back sequentially instead. Also release store file handles after mapping, so many-threaded --store runs no longer abort with 'Failed to open mmap file'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * perf(store): 3.2% less CPU with --store via batched node lookups Gain: callgrind on Luxembourg --store drops from 125.7 to 121.7 G instructions (-3.2%); output identical (all 273,080 Germany tiles). ShardedNodeStore didn't override the batched at(ids, n, out) from #939, so --store looked up every node of every way on its own plus a contains() check, the top entry in a --store profile. Now the whole batch is looked up in the shard of its first node, with per-node lookups only for ways crossing a shard boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(store): don't drop relations crossing the extract boundary (--store output now matches in-memory) With shards, a relation was read only in the pass for the shard of its first member way; if that way wasn't in the extract, every pass skipped it. Germany --store vs in-memory: 119 missing tiles and ~5,700 missing features (Szczecin Lagoon, Usedom, border nature reserves) -> 0 differences in 273,199 tiles. Decide the shard by the first member way that is in the store; relations with no stored member ways are read in the first pass. Bug predates #939. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: dktue <2878623+dktue@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@laem, sorry that this went unanswered. Thanks for the report and the screenshot. Since then we went looking for single-threaded stretches in the planet build and fixed the ones we found (planet, PMTiles, 32 threads):
At 168 GB, an in-memory planet run has usually finished reading, which is where most of these sit: the end of the relations phase (Lake Huron alone took 103 s on one core), then collecting tiles and finalizing. So there's a good chance this is what you saw, and that it's much shorter now. To be sure, could you share:
If it turns out to be something else, we're happy to reproduce it on our 96-thread EPYC with your setup. If you'd like to try the current state in the meantime, |
Setting any malloc parameter freezes glibc's mmap threshold at 128 KB, so every larger buffer is a fresh mmap with zeroed pages. Boost's is_valid grows such vectors over and over for a huge polygon: the Tongass National Forest relation (1.9M points) took 3x as long as before systemed#939. Tongass relation 677 -> 232 s; planet -2.6% wall for +0.85 GB peak heap; planet on 4 CPUs / 16 GB unchanged within noise, same heap peak. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Large clipped polygons such as coastline/ocean shapefiles are written to tiles invalid: scaling them onto the integer tile grid introduces self-intersections. Until now only SIMPLIFIED geometry was repaired, so those layers stayed invalid and can be mis-rendered by strict renderers - as missing water, or as land hidden behind an ocean polygon. New layer option invalid_polygon_repair_scope: "simplified_only" (default, unchanged behaviour) repairs only invalid polygons that simplification may have broken; "all" is a superset and additionally repairs polygons that were merely clipped and quantised. For those, repair_multi_polygon runs with a strict area guard that also rejects any repair GROWING a polygon - growing means a hole was filled, which would make an island disappear. That bound must not apply to simplified geometry, where legitimate repairs do change area; applying it globally measurably made things worse (5 -> 20 invalid features in one tile). The bound is passed as a scoped enum, AreaBound, rather than a bool: other changes also append parameters to repair_multi_polygon (systemed#939 a bool, systemed#949 a bool*), and with a bool here a merge that puts the parameters in the wrong order would still compile, silently. Verified: with the default the output is unchanged (Bangladesh z7 96/55: 166 features, 5 invalid, 319 holes). With "all" on the ocean layers, that tile goes to 4 invalid / 318 holes and the ocean feature behind a reported rendering artefact becomes valid; Denmark z5 16/10 improves 5 -> 1 invalid while Funen correctly stays land; Philippines z5 26/14 improves 7 -> 1 invalid with no land wrongly covered by ocean.
Large clipped polygons such as coastline/ocean shapefiles are written to tiles invalid: scaling them onto the integer tile grid introduces self-intersections. Until now only SIMPLIFIED geometry was repaired, so those layers stayed invalid and can be mis-rendered by strict renderers - as missing water, or as land hidden behind an ocean polygon. New layer option invalid_polygon_repair_scope: "simplified_only" (default, unchanged behaviour) repairs only invalid polygons that simplification may have broken; "all" is a superset and additionally repairs polygons that were merely clipped and quantised. For those, repair_multi_polygon runs with a strict area guard that also rejects any repair GROWING a polygon - growing means a hole was filled, which would make an island disappear. That bound must not apply to simplified geometry, where legitimate repairs do change area; applying it globally measurably made things worse (5 -> 20 invalid features in one tile). The bound is passed as a scoped enum, AreaBound, rather than a bool: other changes also append parameters to repair_multi_polygon (systemed#939 a bool, systemed#949 a bool*), and with a bool here a merge that puts the parameters in the wrong order would still compile, silently. Verified: with the default the output is unchanged (Bangladesh z7 96/55: 166 features, 5 invalid, 319 holes). With "all" on the ocean layers, that tile goes to 4 invalid / 318 holes and the ocean feature behind a reported rendering artefact becomes valid; Denmark z5 16/10 improves 5 -> 1 invalid while Funen correctly stays land; Philippines z5 26/14 improves 7 -> 1 invalid with no land wrongly covered by ocean.

perf: 3.4x faster on the full planet, 3-4.5x on extracts, much less RAM, deterministic output
Full planet: 2 h 10 min -> 38 min 38 s (3.4x faster), 4x less CPU time, 96 GB less peak RAM. On extracts: 3-4.5x faster and 11-20x less peak memory. The tiles are the same apart from small differences in simplified landcover/landuse/water polygons (see Output).
Full planet
Setup: OpenMapTiles profile (
resources/config-openmaptiles.json+process-openmaptiles.lua) with coastline and landcover shapefiles, PMTiles output, in memory (no--store), default thread count. Server: AMD EPYC 9454P (48 cores / 96 threads), 503 GB RAM, Ubuntu 22.04, GCC 11.4. Measured with/usr/bin/time -v, one run per version, back to back on an otherwise idle machine. Both runs wrote all 345,907,442 tiles and exited cleanly.Part of the planet RAM saving comes from closed ways that are only wrongly oriented. These are now kept as reversed references into the way store, instead of being repaired and stored as separate generated polygons: "Generated polygons" in the log falls from 300M to 7.6M.
Extracts
Total CPU time also falls 1.5-1.8x, so the speedup is not just better use of cores; the work itself shrank.
Measurement: interleaved A/B runs (6 pairs per extract) against the parent commit, after a discarded warm-up run. Setup: OpenMapTiles profile, gzip MVT to .mbtiles, 22-thread laptop (Core Ultra 7 165H). Times and RAM are medians.
Output
--threadsvalues differed in up to ~200 Luxembourg tiles, because what the ClipCache held depended on scheduling. Tiles are now split into fixed batches, each with its own clip cache.--materialize-geometries,--store,--shard-storesand--threads 1.Main changes
M_TRIM_THRESHOLDis set unless the user tunes malloc through the environment.boost::geometry::is_valid. Boost still runs whenever the precheck is unsure.test()per tile.Find/Holds/Attribute*/MinZoom/ZOrderare nativelua_CFunctions that keep kaguya's argument-type rules. Eachlua_Stategets a size-class pool allocator.Fast paths were cross-checked against the original code paths during development (that verification code has been removed from this PR).
Not tested
--storemode (only in-memory planet runs were timed).🤖 Generated with Claude Code