fix(lift): write a split chunk's parts before the chunk itself #2751
Closed
github-sync
wants to merge 3 commits from
github/pr/2751 into main
pull from: github/pr/2751
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2751
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2751"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
If a lift worker stops in the middle of cutting a big chunk (killed for memory, which is the case
_eachretries), the functions of that chunk's later parts are removed from the lifted C without a message, and the build then fails far from the cause.Why it happens
_split_one(nx2/chunks.py, from #2324) writes the chunk itself first, cut down to its first part, thencode_NNN_1.c,code_NNN_2.cand so on. When a worker stops between two of those writes:_eachsees the broken pool and runs the file again in the lift's own process, as its docstring says it may, because "each edit finds the places it made already";split_bigthen removes everycode_NNN_k.cthat it did not make in this run, which deletes the parts the dead worker had written.The functions that were in those parts are gone, and nothing says so.
What changed
_split_onewrites the parts from the last to the first, so the chunk itself is written last. A worker stopped part way leaves the chunk whole, and the run again cuts it exactly as before.madekeeps its order (part 1 first), sosplit_big's result andchunk_costs.txtare unchanged.How it was tested
tests/test_chunk_split.pygets a case:whole_write.write_wholeis made to stop_split_oneafter its first file, thensplit_bigruns the chunk again. It checks that the chunk is cut again and that the parts hold every function in order. Onmainit fails ("after a stopped worker the chunk is cut again": 1 part instead of 4); with this change it passes, on Windows and on Linux (WSL).python -I -B tests/run_all.pyon Windows: the same failing set as untouchedmain, none new.I merged this into current main and ran tests/test_chunk_split.py, including the new stopped_part_way case. It passes, and CI is green.
The analysis is right. The old order wrote code_NNN.c (cut down to its first part) first. A worker killed after that left a chunk under budget, so the in-process retry from
_eachmade no parts, and split_big then deleted the code_NNN_k.c files the dead worker had already written. Writing the parts from last to first and the chunk itself last (nx2/chunks.py:414) closes that.made.insert(0, out)keeps the result order the same, so chunk_costs.txt and the build order don't change.One small window is left. If the worker dies after the final write of code_NNN.c but before
_eachgets its result, the retry sees an already-cut chunk again. That's a much narrower window, since the whole chunk is no longer held in memory by then, so I'm fine leaving it.Thanks, the analysis holds up. I read it against
_split_one,split_bigand_each.The bug and the fix. Before, the chunk was rewritten as its first part before the other parts were written. A worker that stopped in between left a chunk under the budget. The in-process retry in
_eachthen made no parts, andsplit_bigremoved everycode_NNN_k.cit had not made in that run. Writing the parts from last to first and the chunk last fixes that. Each write goes throughwhole_write, so no file is ever half written, and a stop at any point leaves a whole chunk that the retry cuts again as before. The unfinished parts are overwritten, or removed as stale if they are not made again. It doesn't interact with the lift cache key (#2686, it only touchesmake_game.py) or the parallel emit (#2748, already merged), because this all happens after the emit, inside the split pass.Byte-identical output. I think so. The cut points and the text of each file don't change.
made.insert(0, out)keeps part 1 first, sosplit_big's return and the build order are as before.costsis now filled in reverse, butwrite_costssorts it, sochunk_costs.txtis the same. The printed line is the same.One window is left, and I think it is wider than Joey-Magnus's review says. He mentions a worker dying after its last write. The same state also occurs when this worker finished all its writes and its result was lost because a different worker was killed. When one worker is killed,
ProcessPoolExecutorbreaks the whole pool, and any future that has no result yet raisesBrokenProcessPool._eachthen runs that chunk again here, finds it already cut, makes no parts, andsplit_bigdeletes the parts the finished worker wrote. That is the same silent loss. It needs two things at once: a memory kill, and a finished worker whose result has not been collected. With 24 chunks in flight at the same time, that is not so narrow. I don't think it has to block this PR, since it is much better than before. A follow-up could make the second run recognise an already-cut chunk. For example, when_split_onesees a chunk under budget that already hascode_NNN_k.cfiles,split_bigkeeps them instead of removing them.Evidence (rule 3c). The new case in
tests/test_chunk_split.pyfails onmainand passes here, which is a good unit-level proof. But this is a build fix, so I'd like to see it in a real build too:genfolder (everycode_*.candchunk_costs.txt) to show the output is identical;mainand the fix, with a worker killed by hand during the split (kill one worker insplit_bigwhile it writes a big chunk) and the hardware named.If the forced kill is hard to do, the hash comparison alone is enough for me. CI is green, and Joey-Magnus has approved. There are no conflicts with other open PRs: nothing else open touches
nx2/chunks.py.I merged main in and added one commit on top for the window from my last review: a worker that finished the cut, but whose result was lost when another worker stopped and broke the pool.
What I added. When
_split_onefinds a chunk under the budget that hascode_NNN_k.cfiles beside it, it joins the chunk and those parts, cuts the result again, and keeps the parts if the cut gives back exactly the same files. So the second run in_eachreturns the parts instead ofsplit_bigdeleting them. Parts from an earlier lift next to a chunk that is now small don't match, so they are still removed. The cut itself moved into_cutwithout changes. One small side effect: a chunk the cut leaves in a single part is no longer rewritten.tests/test_chunk_split.pygets afinished_but_lostcase that covers both cases. It fails without the new commit ("a chunk cut already keeps its parts") and passes with it.Same output. I didn't run a full lift here. Instead I rebuilt the real cut chunks from a cached lift's
genfolder (the 8 chunks that had parts, plus 4 small ones) and split them with main'schunks.pyand with this branch's. Both runs gave the same 30 files, byte for byte, with the same sha256 over all of them and the samechunk_costs.txt. Runningsplit_biga second time over the already-cut files leaves 13 files and none of the 17 parts on main, while this branch keeps all 30 unchanged.test_chunk_split,test_parallel_emit,test_spawn_emit,test_lift_cache,test_chunks_save_folder,test_stale_hot_piecesandtest_failure_reasonall pass.Thanks for tracking this down and for the clear write-up!