Repository navigation
Check that converting through an affine map writes every element once - #8975
Achyuthan-S wants to merge 2 commits into
Conversation
Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Thanks, the mask approach is a clean way to make conversion fail closed without per-element Python. Since this was not run on GPU, I ran it on two machines:
Two gaps against the "every element once" contract: 1. A source view that overlaps itself is still silently wrong. m = ParamAffineMap((6,), {0: (4,), 1: (3,)}, {
0: [AffinePiece((2, 2), 0, (1, 1), 0, (2, 1), [0])], # source addresses 0,1,1,2
1: [AffinePiece((3,), 3, (1,), 0, (1,), [1])]})
m.validate() # passes
m.rebuild({0: torch.tensor([9., 8, 7, 6]), 1: torch.tensor([1., 2, 3])})
# CPU: [9, 7, 6, 1, 2, 3], shard value 8 is dropped, no error
# CUDA: [9, 8, 6, 1, 2, 3], shard value 7 is dropped insteadWhich value survives the aliased write depends on the device, so the same checkpoint converts to different tensors on CPU and GPU. 2. Single-holder vs replica overlap depends on piece order. The single-holder check only looks at the piece being written now, not at who wrote first: p = lambda loc: AffinePiece((4,), 0, (1,), 0, (1,), loc)
shapes = {0: (4,), 1: (4,)}
ParamAffineMap((4,), shapes, {0: [p([0, 1])], 1: [p([1])]}).rebuild(...) # refused
ParamAffineMap((4,), shapes, {1: [p([1])], 0: [p([0, 1])]}).rebuild(...) # acceptedThe same map is refused or accepted depending on |
0z5a
left a comment
There was a problem hiding this comment.
Read at cd35be35. Making conversion refuse instead of returning whatever torch.empty left is the right default.
I re-ran the corruption fuzz with a stricter oracle. The test extracts and rebuilds through the same corrupted map, so an error that cancels between the two goes unnoticed: the self-overlapping source view from jinyouzhi's comment round-trips exactly that way. A real checkpoint holds shards written by the correct layout, so I generated the shards from the uncorrupted map and rebuilt them through the corrupted one. Same generator and seed, 2,000 cases:
| refused | exact | silently wrong | |
|---|---|---|---|
| master | 868 | 711 | 421 |
| this PR | 1,303 | 697 | 0 |
So the claim holds under that oracle too. Switching the test to it would be cheap, a deepcopy of the map before corrupting, and it would catch cancelling errors if the generator ever produces them.
On jinyouzhi's first point: extract has the same gap. With his map, extract(torch.tensor([10., 11, 12, 13, 14, 15]), 0) returns [10, 11, 11, 12] without an error, and that is the AutoTP restore path. A source-overlap check in _validate_rank would cover rebuild and extract together.
On CPU, test_affine_shard_map.py, test_affine_transfer_plan.py, test_zero_affine.py and test_autoep_affine.py pass at this head (263).
…order Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
|
Thanks both, all three points are in the new commit:
New tests:
All of the new tests fail on master. One limit worth stating. A corruption that turns the map into a different valid layout, for example swapping the destinations of two equal-shaped pieces, passes every map check: the map is self-consistent and disagrees only with the memory it describes. That case needs a runtime digest of the data (SyncCheck-style), not a check on the map. |
|
Re-checked at |
Conversion trusts a map further than it checks it.
rebuild(whatds_to_universalcalls on convert) runs onlyvalidate(). That checks each rank's element count and where pieces write in the shard, from metadata, but it never looks at the source side. So a map can pass it while two ranks claim the same block of the parameter and no rank holds another.rebuildthen returns whatevertorch.emptyleft in the elements nobody holds.extract(what the AutoTP restore path calls) runs no check at all.validate()cannot place it. Check where column-split pieces write in a shard #8922 madevalidate_coverage()complete for such destinations, but conversion does not callvalidate_coverage(), because that check walks elements in Python.Reproduction on
master:(8,)parameter.validate()passes.rebuildreturns rank 1's values in elements 0–3 and uninitialised memory in 4–7, with no error.How often this happens:
master, 671 of 2,334 corrupted maps converted without an error and produced a wrong tensor. With this change, none do: each one is refused or still round-trips exactly.validate_coverage()round-trips exactly on both (801 of 801).Changes
rebuildextractrebuild's metadata check for its own rank:validate()restricted to one rank, through a new_validate_rank._validate_rank(and sovalidate())ValueErrorinstead of torch's ownRuntimeError.ValueError, "twice", "uncovered", "different data"), plus "itself" for the new check.Cost:
rebuildalready materialises the full tensor, so the fp32 states thatds_to_universalconverts need at most a quarter more memory, for the duration of one parameter's conversion.Tests
tests/unit/checkpoint/test_affine_shard_map.py:mastertest_conversion_refuses_a_destination_collision_of_any_strideextractandrebuildrefuse the collision from the full-check test of #8922, whichvalidate()acceptstest_replicas_cut_at_different_boundaries_are_compared_in_fulltest_conversion_refuses_a_map_that_does_not_cover_the_parameterrebuildrefuses a map that leaves an element unheld, and one where two ranks own one blocktest_a_replica_overlapping_a_single_holder_is_refused_in_either_ordertest_a_view_that_overlaps_itself_is_refusedextractandrebuildrefuse a source view that reads one address twice, a zero stride, and a destination view that writes one address twicetest_a_corrupted_map_is_refused_or_still_exactThe colliding map of #8922's full-check test is now a helper shared by both tests.
Validation
macOS 26.5 (arm64, CPU), Python 3.9.6, torch 2.8.0,
DS_ACCELERATOR=cpu LOCAL_SIZE=4.test_affine_shard_map.py,test_affine_transfer_plan.py,test_zero_affine.py,test_autoep_affine.pyandtest_autoep_affine_universal.py: 280 passed.test_universal_checkpoint.py,test_autotp_uc_checkpoint.py,test_autoep_checkpoint.py,test_autoep_unit.pyandtest_autoep_affine_universal.py: 344 passed.master(ba0d63ce2) side by side, and it fails exactly the same 57 tests.cpuinforeports noflags).test_storage_maps_match_runtime_ownership_and_plan_into_a_new_placement.ds_to_universal, then a universal load at another TP degree, ZeRO 1, gloo.o_projbias), and Llama with a tied embedding andvocab_parallel_lm_head(vocabulary of 67, uneven at every degree).exp_avgandexp_avg_sqare restored bit for bit, and the resumed losses match the uninterrupted run to within 4.8e-7.yapf0.43.0 andflake8are clean on both files.Refs #8252, #8922.
@delock