@@ -1802,39 +1802,74 @@ def save(
18021802 if filename .endswith (".h5" ) == False :
18031803 raise RuntimeError ("The filename must end with .h5" )
18041804
1805+ local_data = self .data [:]
1806+ local_n = local_data .shape [0 ]
1807+ n_components = self .num_components
1808+
18051809 if h5py .h5 .get_config ().mpi == True and not force_sequential :
1810+ # BUGFIX(#151): the previous parallel path called
1811+ # h5f.create_dataset("data", data=self.data[:])
1812+ # collectively, but each rank passed its own local-sized array.
1813+ # In parallel HDF5 every rank must specify the *same* dataset
1814+ # shape on a collective create_dataset; passing different shapes
1815+ # leaves HDF5's internal metadata inconsistent so the collective
1816+ # close never synchronises, producing a silent hang.
1817+ #
1818+ # Fix: allgather the per-rank sizes, create the dataset at the
1819+ # global shape, then each rank writes its own slice.
1820+ sizes = comm .allgather (local_n )
1821+ total_n = sum (sizes )
1822+ offset = sum (sizes [: comm .rank ])
1823+
18061824 with h5py .File (f"{ filename [:- 3 ]} .h5" , "w" , driver = "mpio" , comm = comm ) as h5f :
18071825 if compression == True :
1808- h5f .create_dataset ("data" , data = self .data [:], compression = compressionType )
1826+ dset = h5f .create_dataset (
1827+ "data" ,
1828+ shape = (total_n , n_components ),
1829+ dtype = local_data .dtype ,
1830+ chunks = True ,
1831+ compression = compressionType ,
1832+ )
18091833 else :
1810- h5f .create_dataset ("data" , data = self .data [:])
1834+ dset = h5f .create_dataset (
1835+ "data" ,
1836+ shape = (total_n , n_components ),
1837+ dtype = local_data .dtype ,
1838+ )
1839+ if local_n > 0 :
1840+ dset [offset : offset + local_n ] = local_data
18111841 else :
1842+ # Sequential fallback: rank 0 creates the file and writes its slab,
1843+ # then each higher rank appends in turn. Indentation here matters —
1844+ # the barrier/loop must be outside the rank-0 branch so all ranks
1845+ # synchronise (the previous version nested them inside, leaving
1846+ # higher ranks' data unwritten and rank 0 deadlocked at the
1847+ # barrier with no peers).
18121848 if comm .rank == 0 :
18131849 with h5py .File (f"{ filename [:- 3 ]} .h5" , "w" ) as h5f :
18141850 if compression == True :
18151851 h5f .create_dataset (
18161852 "data" ,
1817- data = self . data [:] ,
1853+ data = local_data ,
18181854 chunks = True ,
1819- maxshape = (None , self . data . shape [ 1 ] ),
1855+ maxshape = (None , n_components ),
18201856 compression = compressionType ,
18211857 )
18221858 else :
18231859 h5f .create_dataset (
18241860 "data" ,
1825- data = self . data [:] ,
1861+ data = local_data ,
18261862 chunks = True ,
1827- maxshape = (None , self . data . shape [ 1 ] ),
1863+ maxshape = (None , n_components ),
18281864 )
1829- comm .barrier ()
1830- for proc in range (1 , comm .size ):
1831- if comm .rank == proc :
1832- if self .local_size > 0 :
1833- with h5py .File (f"{ filename [:- 3 ]} .h5" , "a" ) as h5f :
1834- incoming_size = h5f ["data" ].shape [0 ]
1835- h5f ["data" ].resize ((h5f ["data" ].shape [0 ] + self .local_size ), axis = 0 )
1836- h5f ["data" ][incoming_size :] = self .data [:, ...]
1837- comm .barrier ()
1865+
1866+ comm .barrier ()
1867+ for proc in range (1 , comm .size ):
1868+ if comm .rank == proc and local_n > 0 :
1869+ with h5py .File (f"{ filename [:- 3 ]} .h5" , "a" ) as h5f :
1870+ incoming_size = h5f ["data" ].shape [0 ]
1871+ h5f ["data" ].resize ((incoming_size + local_n ), axis = 0 )
1872+ h5f ["data" ][incoming_size :] = local_data
18381873 comm .barrier ()
18391874
18401875 ## Add swarm variable unit metadata to the file
@@ -3626,31 +3661,49 @@ def save(
36263661 warnings .warn ("Compression may slow down write times" , stacklevel = 2 )
36273662
36283663 if h5py .h5 .get_config ().mpi == True and not force_sequential :
3629- # It seems to be a bad idea to mix mpi barriers with the access
3630- # context manager so the copy-free version of this seems to hang
3631- # when there are many active cores. This is probably why the parallel
3632- # h5py write hangs
3633-
3664+ # BUGFIX(#151): the previous parallel path called
3665+ # h5f.create_dataset("coordinates", data=points_data_copy)
3666+ # collectively, but each rank passed its own local-sized array.
3667+ # In parallel HDF5 every rank must specify the *same* dataset
3668+ # shape on a collective create_dataset; passing different shapes
3669+ # leaves HDF5's internal metadata inconsistent so the collective
3670+ # close never synchronises, producing a silent hang.
3671+ #
3672+ # Fix: allgather the per-rank sizes, create the dataset at the
3673+ # global shape, then each rank writes its own slice.
36343674 points_data_copy = self ._particle_coordinates .data [:].copy ()
3675+ local_n = points_data_copy .shape [0 ]
3676+ cdim = points_data_copy .shape [1 ]
3677+ sizes = comm .allgather (local_n )
3678+ total_n = sum (sizes )
3679+ offset = sum (sizes [: comm .rank ])
36353680
36363681 with h5py .File (f"{ filename [:- 3 ]} .h5" , "w" , driver = "mpio" , comm = comm ) as h5f :
36373682 if compression == True :
3638- h5f .create_dataset (
3683+ dset = h5f .create_dataset (
36393684 "coordinates" ,
3640- data = points_data_copy ,
3685+ shape = (total_n , cdim ),
3686+ dtype = points_data_copy .dtype ,
3687+ chunks = True ,
36413688 compression = compressionType ,
36423689 )
36433690 else :
3644- h5f .create_dataset ("coordinates" , data = points_data_copy )
3691+ dset = h5f .create_dataset (
3692+ "coordinates" ,
3693+ shape = (total_n , cdim ),
3694+ dtype = points_data_copy .dtype ,
3695+ )
3696+ if local_n > 0 :
3697+ dset [offset : offset + local_n ] = points_data_copy
36453698
36463699 del points_data_copy
36473700
36483701 else :
3649- # It seems to be a bad idea to mix mpi barriers with the access
3650- # context manager so the copy-free version of this seems to hang
3651- # when there are many active cores
3702+ # Sequential fallback: rank 0 creates the file and writes its slab,
3703+ # then each higher rank appends in turn.
36523704
36533705 points_data_copy = self .points [:].copy ()
3706+ local_n = points_data_copy .shape [0 ]
36543707
36553708 if comm .rank == 0 :
36563709 with h5py .File (f"{ filename [:- 3 ]} .h5" , "w" ) as h5f :
@@ -3672,17 +3725,16 @@ def save(
36723725
36733726 comm .barrier ()
36743727 for i in range (1 , comm .size ):
3675- if comm .rank == i :
3728+ if comm .rank == i and local_n > 0 :
3729+ # BUGFIX(#151): the previous version referenced an undefined
3730+ # ``data_copy`` here; passive swarms with a zero-particle
3731+ # rank would have raised NameError. Use the local
3732+ # ``points_data_copy`` we already have.
36763733 with h5py .File (f"{ filename [:- 3 ]} .h5" , "a" ) as h5f :
3677- h5f ["coordinates" ].resize (
3678- (h5f ["coordinates" ].shape [0 ] + points_data_copy .shape [0 ]),
3679- axis = 0 ,
3680- )
3681- # passive swarm, zero local particles is not unusual
3682- if data_copy .shape [0 ] > 0 :
3683- h5f ["coordinates" ][- points_data_copy .shape [0 ] :] = points_data_copy [:]
3734+ existing_size = h5f ["coordinates" ].shape [0 ]
3735+ h5f ["coordinates" ].resize ((existing_size + local_n ), axis = 0 )
3736+ h5f ["coordinates" ][existing_size :] = points_data_copy
36843737 comm .barrier ()
3685- comm .barrier ()
36863738
36873739 del points_data_copy
36883740
0 commit comments