Repository navigation
Byte-for-byte reproducible output: what still differs between multi-threaded runs #969
Replies: 4 comments
|
Thanks for digging into this, @geoneutrino, and for measuring it so carefully. A few data points and thoughts from our side: What we see. In all our Germany comparisons between two runs of the same build (OpenMapTiles profile, Q1: that's really for @systemed to decide. From the verification side, reproducible content (your fixes 1 and 3) would already remove the noise we keep explaining; byte-for-byte on top would mainly bring back the CI tile check. Q2: an idea for objects without an OSM id. Storing the id in Q3: performance shouldn't decide it. From our profiles (Germany, 32 threads), Q4: sorting the pairs once per set in One coordination note: #972 (parallel Offer: when you have a branch or the two prototype commits, we're happy to benchmark them on our server (full planet and Europe, 32–96 threads) and run our semantic tile comparison, so any cost shows up before a decision. |
|
Thanks, this is very helpful. I built a prototype (co-authored and data tested by Claude) along those lines, on current master: https://github.com/geoneutrino/tilemaker/tree/prototype/deterministic-output
Test data results: two runs with all threads and a single-threaded run are byte-identical, with Can turn this into a PR, rebased onto #972 / #966, whichever lands first. |
|
Thanks for the prototype, @geoneutrino. We ran it on the planet and on Germany. Short version: it costs nothing measurable, and together with #942 every tile is reproducible, byte for byte. Planetmaster d0ab14b vs. your prototype on top of it. Planet
Reproducibility (Germany,
|
| tiles with different content | |
|---|---|
| master, 2 runs at 32 threads | 44,317 |
| prototype, 2 runs at 32 threads | 41,624 |
| prototype + #942, 2 runs at 32 threads | 0 of 273,199 |
| prototype + #942, 32 vs. 8 threads | 0 of 273,199 |
With the prototype alone, the remaining differences were all in poi. That's the pairs() order in GetPOIRank() that #942 fixes, which your Lua 5.1 setup can't show. We also tried the prototype with LuaJIT, and poi still differed there, so #942 is needed regardless of the Lua version.
With #942 applied, we also compared the stored tile bytes (compressed, as written) of two 32-thread runs: all 273,199 tiles are byte-identical. The .pmtiles files as a whole still differ (by 1.5 KB here), but only in where each tile sits in the file and in the directory that points to it. The PMTiles writer appends tiles in the order the threads finish them. Writing in a fixed order would make the whole file byte-identical; that's a separate change in the writer.
So from our side: the prototype plus #942 gives reproducible tiles at no measurable cost, and that would let us drop the poi caveat from all our comparisons. We'd be glad to see it as a PR.
One follow-up on the last step to byte-identical files: making the PMTiles writer write tiles in a fixed order (and therefore a fixed directory). Would you like to include that in your PR, or should we take it as a separate PR on top? Either is fine with us. We just don't want to both work on it.
|
Thanks a lot for the planet benchmark! The PR is up: #981 On the PMTiles writer: please take it as a separate PR on top, it fits better with your #966 work and we won't both be working on it. Two notes from looking at it:
Two changes in the PR compared to the prototype you measured: the hashes are now platform-independent ( While verifying, I also found that |
Uh oh!
There was an error while loading. Please reload this page.
We had several aspects of deterministic output / reproducible builds in the last time. In my scenarios i use --threads 1 for testing and comparing but this is a bigger topic e.g. CI job disabled and mentioned in #939 and so on. The following is my current insight discussed with AI and analysed the code. Long story short, i want to collect opinions on this if it would be good to have the reporducible output in all scenarios or not because it would be a bigger task and also needs some decisions how to do it. See below
The
verify-generated-tilesCI job was disabled in bf237fa because "output is not yet byte-for-byte reproducible between runs". #939 made the clip cache deterministic, and #942 / #889 handle the Luapairs()order in POI ranking. I wanted to know what else makes two runs differ, so I measured it. Below are four further sources I found, two of them confirmed with prototypes, plus some open questions about how they should be fixed. I'd rather agree on an approach here before opening a PR.Setup
include_ids: trueandcombine_polygons_below: 14onlandcover/landuse.pairs()issue from Make POI tag key processing deterministic #942 cannot show up in these numbers.--threads 1the output is reproducible. Everything below is about multi-threaded runs on a single machine. I have not looked at differences between platforms or compilers, which the CI job also compared.Results
water_polygonsonly)The last column has fewer identical tiles than the middle one, but that is not a regression. Once the feature order matches, tiles that differ in both feature order and tag order get counted under "inside features" instead. How many tiles source 4 hits also varies from one pair of runs to the next. Neither prototype changed any geometry compared with the other variants, and neither made a measurable difference to runtime (59–67 s per run for all three).
The four sources
All four have the same pattern: an internal index that is handed out in insertion order, and is therefore scheduling-dependent, is used as an ordering key.
1.
objectIDdecides the order inside a merge groupsortOutputObjectIDssorts by layer, z_order, geomType, attributes and finallyobjectID. Objects with identical attributes are the ones that get merged, so inside such a groupobjectIDalone decides the order.objectIDis not the OSM id. It is(store.first << ...) + offset, where the store is athread_local LeasedStoretaken from a shared list on first use (leased_store.h). Both parts depend on scheduling, so the same way gets a differentobjectIDin each run.In
ProcessObjectsthat changes two things: the order ofunion_many(L445), which gives a different ring order and sub-pixel area differences, and which OSM id the merged feature gets, becauseoo = *jtkeeps the last one (L395, L434, L447). This is the only source that changes content. In our run that was 621 tiles inlandcover,landuseandpoi.Prototype, which brought content differences from 621 tiles to 0:
This only helps when there is an OSM id. All profiles shipped in
resources/useinclude_ids: false, andOutputObjectXYdoesn't store an id at all.2.
AttributeIndexdecides the order between attribute groupsattributesin the same comparator is anAttributeIndex: shard plus the position where the set was first inserted (attribute_store.cpp L447–L451), which again depends on which thread got there first. Merging is not affected, because equal sets still have equal indices. But the order of groups within a layer, and so the feature order in the tile, changes in every layer.AttributeSet::hash()is not a stable replacement either, because it combines pair indices (attribute_store.h L261), and those are handed out the same way (L265–L270).Prototype: before sorting, build a content key once per distinct attribute set in the tile (key text, value type, value and minzoom, with pairs sorted by that content), and compare that key instead of the index. This brought the feature-order differences from 12 layers down to
water_polygons. To keep the experiment small it reached theAttributeStorethrough a global pointer set inmain()afterfinalize(), so it is not something I'd propose as it stands.3. Objects without an OSM id are still ordered by
objectIDWhat remains after fixes 1 and 2 is feature order in
water_polygonsonly, which is where the coastline shapefile's polygons go. They have no OSM id, so the tie-break in fix 1 falls through toobjectID.4. Tag order inside a feature
writeAttributesTowrites tags in the orderforEachPairUnsafereturns them, which follows the pair indices. Features, their attributes and their order are then all identical, but the tag order and the layer's key/value tables differ. In one pair of runs that was about a third of all tiles. I assumeMvtAttributeMemofollows the same order, but I have not checked that separately.Questions
OutputObjectXYas well (memory), makeobjectIDindependent of scheduling (e.g. derived from input position instead of the leased store), or for shapefiles use the record number.AttributeStoreintosortOutputObjectIDsand compare by content, or renumberAttributeIndexin content order once afterfinalize()? The second keeps the hot path as it is, but every storedOutputObjectwould need its index remapped.finalizeSet?I'm happy to turn whatever approach you prefer into a PR, and to share the comparison script and the two prototype commits.
All reactions