"MapReduce-MPI WWW Site"_mws - "MapReduce-MPI Documentation"_md :c

:link(mws,http://www.cs.sandia.gov/~sjplimp/mapreduce.html)
:link(md,Manual.html)

:line

Technical Details :h3

This section provides additional details about using the MapReduce
library and how it is implemented.  These topics are covered:

"Length and byte-alignment of keys and values"_#align
"Memory requirements for KeyValue and KeyMultiValue objects"_#memory
"Out-of-core operation"_#ooc
"Fundamemtal library limits"_#limits
"Hash functions"_#hash
"Callback functions"_#callback
"Python overhead"_#python
"Error messages"_#error :ul

:line
:line

Length and byte-alignment of keys and values :link(align),h4

As explained in "this section"_program.html, keys and values are
variable-length strings of bytes.  The MR-MPI library knows nothing of
their contents and simply treats them as contiguous chunks of bytes.

When you register a key and value in your "mymap()"_map.html or
"mycompress()"_compress.html or "myreduce()"_reduce.html function via
the KeyValue "add()"_kv_add.html method, you specify their lengths in
bytes.  Keys and values are typically returned to your program for
further processing or output, e.g. as arguments passed to your
myreduce() function by the "reduce()"_reduce.html operation, as are
their lengths.

Keys and values are passed as character pointers to your functions
where you may need to convert the pointer to an appropriate data type
and then correctly interpret the byte string.  For example, either of
these lines could be used:

int *iptr = (int *) key;
int myvalue = *((int *) key); :pre

If the key or value is a variable-length text string, you may want to
terminate it with a "0", and include the trailing "0" in the byte
count, so that C-library-style string functions can later be invoked
on it.  If a key or value is a complex data structure, your function
must be able to decode it.

A related issue with keys and values is the byte-alignment of integer
or floating point values they include.  For example, it is usually a
bad idea to store an 8-byte double such that it is mis-aligned with
respect to an 8-byte boundary in memory.  The reason is that using a
mis-aligned double in a computation may be slow.

If your keys or values are homogeneous (e.g. all integers), you can
use the {keyalign} and {valuealign} settings, discussed
"here"_settings.html, to insure alignment of keys and values to
desired byte boundaries.  Since this may incur extra memory costs, you
should not typically make these settings larger than needed.

Special care may need to be taken if your values are heterogeneous,
e.g. a mixture of strings and integers.  This is because the MR-MPI
library packs values one after the other into one long byte string
when it is returned to your program as a multi-value, e.g. as an
argument to the callback of a "reduce()"_reduce.html method.  Only the
first value in the multi-value is aligned to the {valuealign}
"setting"_settings.html.  Similarly, the "collapse()"_collapse.html
method creates a multi-value that is sequence of
key,value,key,value,etc from a KV.  If the keys are variable-length
text strings and the values are integers, then the values will not be
aligned on 4-byte boundaries.

Here are two ideas that can be used to insure alignment of
heterogeneous data:

(a) Say your "value" is a 4-byte integer followed by an 8-byte double.
You might think it can be stored and registered as 12 contiguous
bytes.  However, this would likely mean the double is mis-aligned.
One solution is to convert the integer to a double before storing both
quantities in a 16-byte value string.  Another solution is to create a
struct to store the integer and double and use the sizeof() function
to determine the length of the struct and use that as the length of
your "value".  The compiler should then guarantee proper alignment of
each structure member.

(b) Your callback function can always copy the bytes of a key or value
into a local data structure with the proper alignment, e.g. using the
C memcpy() function.  E.g. in the collapse example above, these lines
of code:

int myvalue;
memcpy(&myvalue,&multivalue\[offset\],sizeof(int)); :pre

would load the 4 bytes of a particular value (at location offset) in
the multi-value into the local integer "myvalue", where it can then be
used for computation.

:line

Memory requirements for KeyValue and KeyMultiValue objects :link(memory),h4

KeyValue and KeyMultiValue objects are described in "this
section"_interface_c++.html.  A MapReduce object contains either a
single KeyValue object (KV) or a single KeyMultiValue object (KMV),
depending on which methods you have invoked.

The memory cost for storing key/value pairs in a KV is as follows.
The key and value each have a byte length.  Two integers are also
stored for the key and value length.  There may also be additional
bytes added to align the key and value on byte boundaries in memory;
see the {keyalign} and {valuealign} settings, discussed in "this
section"_settings.html.  Thus the total size of a KV is the memory for
the key/value datums plus 2 integers per pair plus any extra alignment
bytes.

A KMV contains key/multi-value pairs where the number of pairs is
typically the number of unique keys in the original KV.  The memory
cost for storing key/multi-value pairs in a KMV is as follows.  The
key and multi-value each have a byte length.  For the multi-value,
this is the sum of individual value lengths.  Again, there may also be
additional bytes added to align the key and multi-value on byte
boundaries in memory; see the {keyalign} and {valuealign} settings,
discussed in "this section"_settings.html.  Three integers are also
stored: the key and multi-value length, and the number of values N in
the multi-value.  An N-length array of integers is also stored for the
length of each value in the multi-value.  Thus the total size of a KMV
is the memory for the key/multi-value datums plus 3 integers per pair
plus 1 integer per value in the original KV plus any extra alignment
bytes.

Note that memory for key data in a KMV is typically less than in the
original KV, since the KMV only stores unique keys.  The memory for
multi-value data is the same as the value data in the original KV,
since all the original KV values are contained in the multi-values.

Note that in parallel, for a KV or KMV, each processor stores the
above data for only a fraction of key/value pairs it generated during
a "map()"_map.html operation or acquired during other operations, like
a "collate()"_collate.html.  If this is imbalanced, one processor may
own and process datums more than other processors.

If KV or KMV data on a processor exceeds the page size determined by
the {memsize} setting, discussed "here"_settings.html, then data is
written to temporary disk files, on a per-processor basis.

:line

Out-of-core operation :link(ooc),h4

If the KV or KMV pairs of a data set owned by a processor fit within a
single page of memory, whose size is determined by the {memsize}
"setting"_setting.html, then the MR-MPI library operates on the data
in-core; no disk files are written or read.

When the data on any single processor exceeds the page size, that
processor will write data, one page at a time, to one or more
temporary disk files, and later read it back in as needed, again one
page at a time.  Thus all the MR-MPI methods can be invoked on data
sets larger than fit in the aggregate memory of the processors being
used.  The only real limitation in this case is available disk space.

All of the MR-MPI methods, except one, perform their operations within
a fixed number of memory pages.  This includes memory needed for
message passing calls to the MPI library, e.g. buffers used to send
and receive data.  Any large data exchanges are performed with
pre-posted receives (MPI_Irecv) into user-space memory, which do not
require additional internal MPI library memory.

The number of required pages ranges from 1 to 7, and is listed on
"this page"_interface_c++.html for each MR-MPI library method.  This
means, for example, that even if the page size is 1 Mb (smallest
allowed value), and the data set size is 10 Gb per processor, and the
"sort_keys()"_sort_keys.html method is invoked, which requires 5 pages
per processor, that the operation will successfully complete, using
only 5 Mb per processor.  Of course, there may be considerable disk
I/O performed along the way.

The one exception is the "convert()"_convert.html method, also called
by the "collate()"_collate.html and "commpress()"_compress.html
methods, which performs an on-processor reorganization of the data in
a KV to produce a KMV.  For large data sets this requires breaking up
the large KV data file into smaller files, each of which holds data
that will contribute to one page of the eventual KMV file.  Each
smaller file requires an in-memory buffer to store data that is
written to the file.  The number of these smaller files, and hence the
number of buffers, is hard to predict in advance or even bound.  It
depends on the page size and the characteristics of the KV pairs,
e.g. how many unique keys there are.  The number of extra allocated
pages needed to store these buffers depends of the number of small
files and the minimum buffer size, which is currently set at 16K bytes
for reasonable disk I/O performance.  If a very large number of small
files are needed to partition the KV data and the page size is small,
then several extra memory pages may need to be allocated.  This is not
normally the case, but the number of small files and number of
allocated pages can be monitored if the {verbosity}
"setting"_setting.html is non-zero.  Note that a larger page size will
reduce the number of extra pages the "convert()"_convert.html method
needs to allocate.

IMPORTANT NOTE: You should choose a {memsize} "setting"_setting.html
that insures the total memory consumed by all pages allocated by all
the MapReduce objects you create, does not exceed the physical memory
available (which may be shared by several processors if running on a
multi-core node).  If you do this, then many systems will allocate
virtual memory, which will typically cause MR-MPI library operations
to run very slowly and thrash the disk.

Also note that in addition to "pages", there are numerous additional
small allocations of memory made by the MR-MPI library.  Here are two
examples.  The "aggregate()"_aggregate.html method allocates vectors
of length P = the number of processors.  Out-of-core disk files are
stored as "pages" of data.  Each page requires some in-memory
bookkeeping so it can be written and read.  Thus if a file grows to
1000s of pages, the corresponding in-memory bookkeeping structure will
also become larger.  For normal page sizes as determined by the
{memsize} "setting"_setting.html, e.g. the 64 Mbyte default, these
additional in-memory allocations should be small compared to the size
of a single page.

:line

Fundamemtal library limits :link(limits),h4

Even in out-of-core mode, the MR-MPI library has limitations on the
data set sizes it can process.  In practice, these are hopefully not
restrictive limits.

Define:

INTMAX = 2^31 - 1 = largest 32-bit signed int
UINT64MAX = 2^64 - 1 = largest 64-bit unsigned int
pagesize = size (in bytes) of 1 page of memory :ul

Internal storage limits within library:

KV = KeyValue, KMV = KeyMultiValue

UINT64MAX = max byte count of KV or KMV data across all procs
UINT64MAX = max # of KV or KMV pairs across all procs
UINT64MAX = max # of values in a single KMV pair
UINT64MAX = max pagesize
min(pagesize,INTMAX) = max size of 1 KV pair
INTMAX = max number of KV or KMV pairs in one page (on a processor)
INTMAX = max # of values in single KMV pair, before split across pages
INTMAX = max summed value size in single KMV pair, before split across pages :ul

Additional notes:

The user sets the "pagesize" via the {memsize} setting, in Mbytes.
The pagesize can exceed INTMAX, though it should not exceed the
physical memory available.  See the "discussion above"_#ooc for more
details.

Since the data set size is written to disk, when the library operates
in out-of-core mode, the data size cannot exceed available disk space,
either on a per-processor basis (if each processor is writing to its
own local disk), or in aggregate (e.g. for a parallel file system).
Some MR-MPI operations convert data from one form to another (e.g. KV
to KMV) or make intermediate copies of data (e.g. for sorting).  At a
minimum this typically requires 2x the disk space of the data set
itself.

As discussed "here"_#align, a KeyValue pair requires 2 integers plus
the key and value, plus alignment space.  For a 1-byte key and a
0-byte value, this is a minimum of 12 bytes.  By storing no more than
INTMAX KeyValue pairs on a page, this still allows for pagesizes of
nearly 24 Gb, more if KeyValue pair sizes are larger.

The various INTMAX limits mean that user calls to the library, and
library callbacks to user functions can use int parameters rather than
uint64 parameters.  It also reduces storage requirements for
individual KeyValue and KeyMultiValue pairs.  One exception is that
all the library methods return a uint64 for the final number of
KeyValue or KeyMultiValue pairs stored by the library.  Another
exception is the uint64 "itask" variable passed back to one flavor of
the user mymap() function via the "map()"_map.html method.

The INTMAX limits on the number of KeyMultiValue values stored in one
page, mean that individual KeyMultiValue pairs that exceed this will
be split across multiple pages.  The user callback functions access
these pages via the multivalue_blocks() and multivalue_block()
methods, described witht the "reduce()"_reduce.html method.

:line

Hash functions :link(hash),h4

The "convert()"_convert.html and "collate()"_collate.html methods use
a hash function to organize keys and find duplicates.  The MR-MPI
library uses the hashlittle() function from lookup3.c, written by Bob
Jenkins and available freely on the WWW.  It operates on
arbitrary-length byte strings (a key) and produces a 32-bit integer
hash value, a portion of which is used as a bucket index into a hash
table.

:line

Callback functions :link(callback),h4

Several of the library methods take a callback function as an
argument, meaning that function is called back to from the library
when the method is invoked.  These functions are part of your
MapReduce program and can perform any operation you wish on your data
(or on no data), so long as they produce the appropriate information.
E.g. they generate key/value pairs in the case of "map()"_map.html or
"compress()"_compress.html or "reduce()"_reduce.html, or they hash a
key to a processor in the case of "aggregate()"_aggregate.html or
"collate()"_collate.html, or they compare two keys or values in the
case of "sort_keys()"_sort_key.html or
"sort_values()"_sort_values.html.

The mymap() and myreduce() functions can perform simple operations or
very complex, compute-intensive operations.  For example, if your
parallel machine supports it, they could invoke another program or
script to read/parse an input file or calculate some result.

Note that in your program, a callback function CANNOT be a class
method unless it is declared to be "static".  It can also be a
non-class method, i.e. just a stand-alone function.  In either case,
such a function cannot access class data.

One way to get around this restriction is to define global variables
that allow your function to access information it needs.

Another way around this restriction is to use the feature provided by
several of the library methods with callback function arguments which
allow you to pass in a pointer to whatever data you wish.  This
pointer is returned as an argument when the callback is made.  This
pointer should be cast to (void *) when passed in, and your callback
function can later cast it back to the appropriate data type.  For
example, a class could set the pointer to an array or an internal data
structure or the class itself as "(void *) this".  Specify a NULL if
your function doesn't need the pointer.

:line

Python overhead :link(python),h4

Using the MR-MPI library from Python incurs two not-so-obvious
overheads beyond the usual slowdown due to using an interpreted
language.  First, Python objects used as keys and values are "pickled"
and "unpickled" using the cPickle Python library when passed into and
out of the C++ library.  This is because the library stores them as
byte strings.  The pickling process serializes a Python object
(e.g. an integer, a string, a tuple, or a list) into a byte stream in
a way that it can be unpickled into the same Python object.

The second overhead is due to the complexity of making a double
callbacks between the library and your Python script.  I.e. the
library calls back once to the user program which then calls back into
the library.  Consider what happens during a map() operation when the
library is called from a C++ program.

the program calls the library map() method
the library map() calls back to the user map() callback function
the user map() calls the library add() method to register a key/value pair :ul

When doing this from Python there are 3 additional layers between the
Python program and the library, the Python mrmpi class, an invisible C
layer (created by ctypes), and the C interface on the C++ library
itself.  Thus the callback operation proceeds as follows:

the program calls the mrmpi class map() method
the mrmpi class map() calls the invisible C map() function
the invisible map() calls the C interface map() function
the C interface map() calls the library map() method
the library map() calls back to the invisible C callback function
the invisible callback calls the mrmpi class callback method
the mrmpi callback calls the user map() callback function
the user map() calls the mrmpi class add() method to register a key/value pair
the mrmpi class add() calls the invisible C add() function
the invisible add() calls the C interface add() function
the C interface add() calls the library add() method :ul

Thus 3 calls have become 11 due to the 3 additional layers data must
pass through.  Some of these pass throughs are very simple, but others
require massaging and copying of data, like the pickling/unpickling
described above, which occurs in the mrmpi class methods.  I was
somewhat surprised this double-callback sequence works as well and as
transparently as it does - Python ctypes is amazing!

:line

Error messages :link(error),h4

The error messages printed out by the MR-MPI library are hopefully
self-explanatory.  At some point they will be listed in these doc
pages.
