Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
269af4e
add parallel merge using mpi
adammoody Aug 9, 2021
9ba081b
handle case where some ranks might have 0 items
adammoody Aug 10, 2021
d29a702
add inclusive scan prefix sum
adammoody Aug 11, 2021
ed49713
report more timing info
adammoody Aug 11, 2021
e94f2a0
Update megatron/data/indexed_dataset.py
adammoody Aug 12, 2021
687ff32
Update megatron/data/indexed_dataset.py
adammoody Aug 12, 2021
af59545
rename total size variable for clarity
adammoody Aug 12, 2021
4f648a0
move translation to bin/idx file names a level deeper
adammoody Aug 13, 2021
9f2ba6a
parallel merge for cached dataset
adammoody Aug 13, 2021
72d6c9c
add alltrue function
adammoody Aug 13, 2021
8b67bec
move collectives to new distdata class, add torch.distributed
adammoody Aug 14, 2021
3eca1f3
drop unused prefix_sum function
adammoody Aug 14, 2021
a691b48
allow ranks to pass a list of files to be merged
adammoody Aug 15, 2021
e4a34e2
check that input dataset files exist
adammoody Aug 15, 2021
8b168ca
fix: using wrong doc_idx list for mmap
adammoody Aug 16, 2021
7a02693
move init dist and collectives to distdata class
adammoody Aug 16, 2021
eca2940
add --merge option, move parallel/serial to their own functions
adammoody Aug 16, 2021
b14491d
Merge branch 'main' into pmerge
adammoody Aug 16, 2021
ec11281
Update megatron/data/distdata.py
adammoody Aug 16, 2021
354d13b
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
2dc3f7a
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
980e904
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
ebd20a6
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
69b2f49
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
50de06a
Update megatron/data/indexed_dataset.py
adammoody Aug 16, 2021
af290ad
drop extraneous numpy tolist calls
adammoody Aug 16, 2021
4b58c74
rename self.MPI to mpi4py
adammoody Aug 16, 2021
71a2fdc
handle case where no ranks have elements in their file
adammoody Aug 16, 2021
73d3a24
rename tokenize_start to time_start
adammoody Aug 16, 2021
b9e69be
drop unrelated comment in distdata.min
adammoody Aug 16, 2021
da615c6
add comment why pointers_shift is not None and add assert
adammoody Aug 16, 2021
c42f41f
note why pointers uses sizes count and offset values
adammoody Aug 16, 2021
a3a7d53
can just rely on rank 0 for the leading 0 element
adammoody Aug 17, 2021
163310a
add write_list function
adammoody Aug 17, 2021
01b2be0
determine element size
adammoody Aug 17, 2021
4b6e8ff
add checks for consistent element_size values
adammoody Aug 17, 2021
ea08555
check that at least one rank has a file to merge
adammoody Aug 17, 2021
2524fce
assert that torch backend is gloo or mpi
adammoody Aug 17, 2021
ca14d48
add collectives for assert and raise
adammoody Aug 17, 2021
d482f36
rename to allassert and allraise_if
adammoody Aug 17, 2021
28d76f5
check dtype instead of element_size
adammoody Aug 17, 2021
f706108
add uint32 to element_sizes table
adammoody Aug 17, 2021
f122883
infer dtype from files being merged
adammoody Aug 17, 2021
57c012e
add write_header function to indexed dataset classes
adammoody Aug 17, 2021
eed8327
call write_header internally from IndexedDataset classes
adammoody Aug 17, 2021
a75cfc2
return number of bytes written from write calls
adammoody Aug 17, 2021
afcfcf9
Merge branch 'main' into pmerge
adammoody Aug 17, 2021
74b733a
move scatterv to distdata class
adammoody Aug 17, 2021
dadb51b
add functions to format status and error messages
adammoody Aug 17, 2021
a2f8fa0
defer merge_files_dist to future PR
adammoody Aug 17, 2021
39e6cd7
open files using with, refresh comments
adammoody Aug 18, 2021
2a29d99
rely on default torch datatypes
adammoody Aug 18, 2021
d6fa895
fix some status messages from preprocess script
adammoody Aug 18, 2021
1216c0a
fix: exclusive scan computing pointers list
adammoody Aug 18, 2021
a64d3da
Merge branch 'pointerfix' into pmerge
adammoody Aug 18, 2021
fde439e
fix: exclusive scan to compute mmap pointers list
adammoody Aug 18, 2021
ba14351
note about seek
adammoody Aug 19, 2021
852fdd0
rename preprocess_dataset_mpi.py to preprocess_data_dist.py
adammoody Aug 19, 2021
61f4b46
update usage comments at top of script
adammoody Aug 19, 2021
22400f3
restore commented print_rank_0 statements
adammoody Aug 19, 2021
5cfcb95
restore status message in mmap merge_file_
adammoody Aug 19, 2021
74c4883
drop mpi4py, sad :(
adammoody Aug 19, 2021
373e514
Merge branch 'main' into pmerge
adammoody Aug 19, 2021
78ab715
add test case for parallel merge
adammoody Aug 19, 2021
002b403
add preprocess_data_dist test for serial merge
adammoody Aug 19, 2021
ba763f7
improve error handling
adammoody Aug 20, 2021
fa11159
refactor get_pointers code
adammoody Aug 20, 2021
7e53fd3
bug fix in exscan
adammoody Aug 20, 2021
53df36f
further refactor get_pointers
adammoody Aug 20, 2021
c43348f
move exscan collective for pointers outside of try block
adammoody Aug 20, 2021
81c21dd
clarify some comments
adammoody Aug 20, 2021
adee502
include string 1k in name of test files
adammoody Aug 20, 2021
13ae421
use temporary file for index
adammoody Aug 20, 2021
f3e1b1d
fix: implement scatterv from torch.distributed.scatter
adammoody Aug 23, 2021
42962e1
switch to pad method in torch.nn.functional
adammoody Aug 25, 2021
9a2f383
return data received in scatterv as new tensor
adammoody Aug 25, 2021
15b7603
raise exception if conflicting scratch and merge options
adammoody Aug 25, 2021
4adaddd
use allraise method from distdata in preprocess_data_dist
adammoody Aug 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 216 additions & 6 deletions megatron/data/indexed_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,21 +417,21 @@ def __init__(self, path, skip_warmup=False):
offset = stream.tell()

if not skip_warmup:
print_rank_0(" warming up index mmap file...")
# print_rank_0(" warming up index mmap file...")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree that it'd be useful to have these. When running with lots of procs, these status messages clutter up the output quite a bit, and they print in a random order since each process prints them independently.

It'd be good to have a way to silence them. For now, I commented them out for testing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't rank_0 force only one process to print? After reading the code this seems to work only to torch.distributed. I'd be a favor of dropping mpi4py if it doesn't bring substantial performance improvement.

_warmup_mmap_file(path)

self._bin_buffer_mmap = np.memmap(path, mode='r', order='C')
self._bin_buffer = memoryview(self._bin_buffer_mmap)
print_rank_0(" reading sizes...")
# print_rank_0(" reading sizes...")
self._sizes = np.frombuffer(
self._bin_buffer,
dtype=np.int32,
count=self._len,
offset=offset)
print_rank_0(" reading pointers...")
# print_rank_0(" reading pointers...")
self._pointers = np.frombuffer(self._bin_buffer, dtype=np.int64, count=self._len,
offset=offset + self._sizes.nbytes)
print_rank_0(" reading document index...")
# print_rank_0(" reading document index...")
self._doc_idx = np.frombuffer(self._bin_buffer, dtype=np.int64, count=self._doc_count,
offset=offset + self._sizes.nbytes + self._pointers.nbytes)

Expand Down Expand Up @@ -576,8 +576,8 @@ def merge_file_(self, another_file):
index = MMapIndexedDataset.Index(index_file_path(another_file))
assert index.dtype == self._dtype

total_len = len(index.sizes)+len(self._sizes)
print(f" concat {another_file} size={len(index.sizes)} for a total size of {total_len}")
# total_len = len(index.sizes)+len(self._sizes)
# print(f" concat {another_file} size={len(index.sizes)} for a total size of {total_len}")

offset = len(self._sizes)
self._sizes.extend(index.sizes)
Expand All @@ -592,3 +592,213 @@ def finalize(self, index_file):

with MMapIndexedDataset.Index.writer(index_file, self._dtype) as index:
index.write(self._sizes, self._doc_idx)


def mpi_get_sum(val, mpi, comm):
"""Compute sum of val, and return total on all ranks."""
insize = np.array([val], dtype=np.int64)
outsize = np.zeros_like(insize)
comm.Allreduce(insize, outsize, op=mpi.SUM)
return outsize[0]


def mpi_get_offset(val, mpi, comm):
"""Compute preifx sum (exclusive scan) of val, and return offset of each rank."""
Comment thread
adammoody marked this conversation as resolved.
Outdated
insize = np.array([val], dtype=np.int64)
outsize = np.zeros_like(insize)
comm.Scan(insize, outsize, op=mpi.SUM)
offset = outsize[0] - insize[0]
return offset


def mpi_get_min(val, mpi, comm):
Comment thread
thomasw21 marked this conversation as resolved.
Outdated
"""Return minimum val to all ranks."""
insize = np.array([val], dtype=np.int64)
outsize = np.zeros_like(insize)
comm.Allreduce(insize, outsize, op=mpi.MIN)
return outsize[0]


def mpi_create_file(filename, mpi, comm):
"""Create, truncate, and open a file shared by all ranks."""
# Don't truncate file until all ranks reach this point
comm.barrier()
Comment thread
thomasw21 marked this conversation as resolved.
Outdated

# Wait for rank 0 to open (and truncate) file,
# then have all ranks open file for writing.
rank = comm.Get_rank()
if rank == 0:
f = open(filename, 'wb')
comm.barrier()
if rank != 0:
f = open(filename, 'r+b')

# TODO: verify that all ranks successfully opened the file
comm.barrier()
Comment thread
thomasw21 marked this conversation as resolved.
Outdated

return f


# To create the binary files given a set of per-rank binary
# files, one simply concatenates the data from the per-rank
# binary files in rank order. We stat each rank file to determine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember you saying something about stat taking some time before being updated correctly? Am I hallucinating, or do we have potentially a race condition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the backing file system being used and also how the procs access the files. On file systems like Lustre and GPFS, the stat info will be up-to-date and consistent to all procs on all nodes.

On a file system like NFS, procs on one node may see a different file size than procs on another node due to client-side caching of file metadata. However, even on NFS, the stat info should be up-to-date and correct from the process where the file was written, and the algorithm used here satisfies that.

Having said all of that, writing any type of shared file in NFS is risky (not well-supported). I think the stance we should take is to say this is perfectly safe for Lustre/GPFS. It may or may not work on NFS -- users can try, but they do so at their own risk.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum so I'm not familiar with these notions, I'm guessing JZ is Lustre or GPFS. After reading the doc, it seems to be the case:

Spectrum Scale system of parallel files (ex-GPFS)
Parallel storage device with SSD disks (GridScaler GS18K SSD) with a capacity of 1 PB

If so, can you add something at the top of file in order to document that? And maybe someone with more expertise on this can take a look. I guess I'll run the code a few times, and check that it matches the vanilla version.

# its size, execute a scan to compute the byte offset where
# the calling rank should write its data, seek to proper
# spot, and copy the full file.
def merge_files_mpi_bin(outfile, infile, mpi, comm):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I think the main trick is that binaries are just concatenated, which allows to leverage mpi. I think mpi works well for your mpi PR. But can you implement the same using multiprocessing? Ie add a new method merge_files_bin_ that uses multiprocessing using the same trick. The one caveat of mpi is it uses mpi ^^'

If you do work with multiprocessing you might interest yourself in the case the number of files differs from the number of workers you have. Essentially it should use something like Pool.map or something like that.

Also you'd be able to refactor merge_file to be special case of merge_files_bin_ + merge_file_idx.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please check it does the same thing for mmap and indexed dataset. I'm not sure in both case the merge is just the concatenation of bin files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I've only looked at mmap files so far. I'm guessing the other file has a different file format, and so does not yet work with this.

@adammoody adammoody Aug 12, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another key trick here is in computing the offsets. I think a multi-processing option should work, but there just needs to be a compute the various offsets for the starting byte offset of each file.

One option might be to just have the main process stat each file and fill in a list of filesizes. Each process in the python multiprocess pool could then identify the subset of files it is responsible for and check the list to compute the offsets for each of those files.

@thomasw21 thomasw21 Aug 16, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as long as the "merge" part is done in multiprocessing, it should still be quite fast no? If all you need is to cumsum or max, it should be fast on a single process, and you could dispatch it to everyone easily no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that should be possible. Depending on the IndexedDataset type, there will be some number of values for which one needs cumsum lists and totals. Those values can be accumulated on the main process and dispatched to the workers. Then each worker process should be able to directly writes its subset of files into the final merge file. I've refactored the gather_files functions such that it's easy to see those values. I think one would just need to refactor that a bit further to pass all of those values in as parameters. This particular PR is getting long enough that I'd recommend pushing that to another PR, though.

"""Concatenate per-rank binary files into a new file given by outfile"""
import stat
import shutil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can go as the top now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll move those up.


comm.barrier()
Comment thread
thomasw21 marked this conversation as resolved.
Outdated

# Create shared output file.
f = mpi_create_file(outfile, mpi, comm)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we can use the with syntax. It forces more indent, but it allows not to forgot to close fle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so. I'll take a look.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got the with syntax in there now.


# get file size of binary file for this rank
filesize = os.stat(infile)[stat.ST_SIZE]

# compute offset this rank should start copying
# its data into the merged file
offset = mpi_get_offset(filesize, mpi, comm)

# seek to appropriate offset and copy data
f.seek(offset)
with open(infile, "rb") as fsrc:
shutil.copyfileobj(fsrc, f)

f.close()

# TODO: check that all ranks wrote successfully

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you could check resulting bin s.t it's size is equal to the sum of all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some error handling here to verify that all ranks wrote successfully. If any rank throws an exception, they all will.

comm.barrier()
Comment thread
thomasw21 marked this conversation as resolved.
Outdated


def merge_files_mpi_idx(outfile, infile, mpi, comm, dtype):
rank = comm.Get_rank()
numranks = comm.Get_size()

comm.barrier()

# Create shared output file
f = mpi_create_file(outfile, mpi, comm)
Comment thread
thomasw21 marked this conversation as resolved.
Outdated

# Read the index file for the calling rank
index = MMapIndexedDataset.Index(infile)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be generic, can be either MMapIndexDataset or the other one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we'll definitely want to support the other format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added support for regular IndexedDataset. I've added that as a separate function. This function and the one for MMapIndexedDataset can both be refactored once we an API to support partial writes.

sizes = index.sizes
if rank == 0:
docs = index.doc_idx

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So careful, this has a strong assumption that rank=0 has a bin file offset of 0. Though it's guaranteed currently, make sure to document somewhere.

if rank != 0:
docs = index.doc_idx[1:]

# Compute total number of size and document index
# values across all ranks. Also compute the offset
# of the calling rank for each value considering
# the values of sizes/docs for all ranks before the
# calling rank.
numsizes = len(sizes)
numdocs = len(docs)
size_count = mpi_get_sum(numsizes, mpi, comm)
docs_count = mpi_get_sum(numdocs, mpi, comm)
Comment thread
adammoody marked this conversation as resolved.
Outdated
size_offset = mpi_get_offset(numsizes, mpi, comm)
docs_offset = mpi_get_offset(numdocs, mpi, comm)

# Have rank 0 write the file header
pos = 0
if rank == 0:
f.write(MMapIndexedDataset.Index._HDR_MAGIC)
f.write(struct.pack('<Q', 1))
f.write(struct.pack('<B', code(dtype)))
f.write(struct.pack('<Q', size_count))
f.write(struct.pack('<Q', docs_count))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here we want to abstract it to be generci to the other dataset index. Essentially we have to create a new class, something along the line of partial write. It needs to support the following;

  • initialize() -> ran only on rank=0 where it adds all the header
  • seek(offset) -> Essentially you know where you need to add the list.
  • write() -> current write function.

pos = f.tell()

# Broadcast value of pos from rank 0,
# and advance file position past file header on all ranks.
pos = mpi_get_sum(pos, mpi, comm)

# The list of size values from each rank are
# concatenated and stored as int32.
f.seek(pos + size_offset * np.int32().itemsize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we abstract this into write? Essentially write needs 4 things:

  • sizes
  • docs
  • offset_sizes
  • offset_counts

sizes32 = np.array(sizes, dtype=np.int32)
f.write(sizes32.tobytes(order='C'))
del sizes32
pos += size_count * np.int32().itemsize
Comment thread
thomasw21 marked this conversation as resolved.
Outdated

# The pointer values store the byte offset to each sentence.
# A sentence has a variable number of tokens, given by
# its corresponding entry in the size array. Each token
# of a sentence is stored in units of type dtype, which consumes
# dtype().itemsize bytes (often a standard type that is just
# large enough to represent all elements of the vocabulary).

# First compute byte offsets for each sentence of our
# local set of sentences.
pointers = np.array(sizes, dtype=np.int64)
pointer_last = 0
if len(sizes) > 0:
np.cumsum(pointers, axis=0, out=pointers)
pointers *= dtype().itemsize
pointer_last = pointers[-1]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not reuse the get_pointers method.


# Then account for bytes for all sentences on ranks
# before the calling rank.
pointer_offset = mpi_get_offset(pointer_last, mpi, comm)
pointers += pointer_offset

# Finally, zero-base the offset values by subtracting
# the number of bytes of the first sentence. To do that
# we first need to find the rank having the first sentence,
# then bcast that size to all ranks.
if size_count > 0:
# There is at least one sentence across all ranks,
# figure out which rank has the first sentence which
# is not necessarily rank 0.
minrank = numranks
if len(sizes) > 0:
minrank = rank
minrank = mpi_get_min(minrank, mpi, comm)

# Broadcast size of the first sentence from minrank.
# We "bcast" using all allreduce.
pointers_shift = 0
if minrank == rank:
pointers_shift = pointers[0]
pointers_shift = mpi_get_sum(pointers_shift, mpi, comm)

# Zero-base pointers by subtracting size of first
# sentence from all values.
pointers -= pointers_shift

# Seek to proper offset for this rank and write
# pointer values into file, stored as int64.
f.seek(pos + size_offset * np.int64().itemsize)
f.write(pointers.tobytes(order='C'))
del pointers
pos += size_count * np.int64().itemsize

# The document index points to the position in the sizes
# array for the starting sentence of each document.
# A variable number of sentences can be in each document.
# Adjust document index for number of sentences that
# come before the calling rank.
doc_idx = np.array(docs, dtype=np.int64)
doc_idx += size_offset

# Seek to proper offset for this rank and write
# document index into file, stored as int64.
f.seek(pos + docs_offset * np.int64().itemsize)
f.write(doc_idx.tobytes(order='C'))
del doc_idx

f.close()

# TODO: check that all ranks wrote successfully
comm.barrier()


def merge_files_mpi(filemain, filerank, mpi, comm, dtype=np.int64):
# Concatenate the data files
merge_files_mpi_bin(data_file_path(filemain), data_file_path(filerank), mpi, comm)

# Combine index files into a single index file
merge_files_mpi_idx(index_file_path(filemain), index_file_path(filerank), mpi, comm, dtype)
Loading