Provided by: pdl_2.100-1_amd64 

NAME
PDL::Parallel::threads::SIMD - launch and synchronize Single-Instruction-Multiple-Dataset code
VERSION
This documentation describes version 0.02 of PDL::Parallel::threads::SIMD.
SYNOPSIS
use PDL::Parallel::threads::SIMD qw(parallelize parallel_sync parallel_id);
# Launch five threads that all print a statement
parallelize {
my $pid = parallel_id;
print "Hello from parallel thread $pid\n";
} 5;
my @positions :shared;
# Run 47 time steps, performing the calculations
# for the time steps in parallel
my $size = 100;
my $N_threads = 10;
my $stride = $size / $N_threads;
parallelize {
my $pid = parallel_id;
# Set this thread's portion of the positions to zero
my $start = $stride * $pid;
my $end = $start + $stride - 1;
@positions[$start..$end] = (0) x $stride;
for (1..47) {
# First make sure all the threads are lined up
parallel_sync;
# Now calculate the next positions
$positions[$_] += $velocities[$_] for $start .. $end;
}
} $N_threads;
DESCRIPTION
In my experience, parallel algorithms are nearly always expressed in a form called single-instruction,
multiple-dataset (SIMD). That is, the exact same code runs in multiple threads, and the only difference
between the threads is the data they manipulate. This is certainly the case for MPI and CUDA, two high-
performance parallel computing frameworks. The goal of this module is to provide a means for you to write
single-machine SIMD code in Perl. It won't be as performant as MPI, CUDA, or OpenCL, but with a little
work I hope it can give decent results. In the very least, I hope it can serve as a good pedagogical tool
for understanding parallel algorithms.
SIMD code needs three facilities: a fast mechanism for data sharing, a means to enforce barrier
synchronization, and an indication of which thread is which, typically in the form of a thread id. This
module provides a way to realize the second and third of these; data sharing is already available thanks
to general Perl data sharing (not fast, but it is easy to share data) and PDL::Parallel::threads, which
provides a simple way to share PDL data across threads in a way that is quite fast.
The main element that this module provides is the "parallelize" function, which allows for a simple and
obvious specification for your SIMD code. From within the block, you can obtain the parallelized thread
id, which is a block-specific number between 0 and one less the number of threads executing in your
parallelized block. You obtain the parallel thread id by calling "parallel_id". Also from within the
block, you can enforce a barrier synchronization point using "parallel_sync".
For example, here's a complete working script that demonstrates the use of "parallelize" and
"parallel_id":
use PDL::Parallel::threads::SIMD qw(parallelize parallel_id);
parallelize {
my $pid = parallel_id;
print "Hello from parallel thread $pid\n"
} 10;
When I run this on my machine, I get this output:
Hello from parallel thread 1
Hello from parallel thread 2
Hello from parallel thread 3
Hello from parallel thread 4
Hello from parallel thread 5
Hello from parallel thread 6
Hello from parallel thread 7
Hello from parallel thread 8
Hello from parallel thread 0
Hello from parallel thread 9
Look closely at that output and you should notice that between thread 8 and 9 comes thread 0. In general,
parallel threads have no guarantee of ordering and for longer parallelized blocks the eventual order for
such a printout is often essentially random.
As you can see, the block that you provide to "parallelize" gets executed ten times, but within each
block the value returned by "parallel_id" is a unique integer between 0 and 9. Perl assigns a unique id
to every thread that it runs, so my use of the phrase parallel thread ids here is a deliberate way to
distinguish this id from Perl's thread id. Perl's thread id will incrementally increase throughout the
life of your program, increasing with each thread that you spawn, but the parallel thread id will always
begin counting from zero for a given parallelized block.
Why would you want each thread to have a different number that is distinct from its Perl-assigned thread
id? The reason is that having such unique, sequential, and normalized numbers makes it very easy for you
to divide the work between the threads in a simple and predictable way. For example, in the code shown
below, the bounds for the slice are calculated in a thread-specific fashion based on the parallel thread
id.
use PDL;
use PDL::Parallel::threads;
use PDL::Parallel::threads:SIMD qw(parallelize parallel);
use PDL::NiceSlice;
# Load the data with 7 million elements into $to_sum...
# ...
# Share it.
$to_sum->share_as('to-sum');
# Also allocate some shared, temporary memory:
my $N_threads = 10;
zeroes($N_threads)->share_as('workspace');
my $stride = $to_sum->nelem / $N_threads;
# Parallelize the sum:
parallelize {
my $pid = parallel_id;
my ($to_sum, $temporary)
= retrieve_pdls('to-sum', 'workspace');
# Calculate the thread-specific slice bounds
my $start = $stride * $pid;
my $end = $start + $stride - 1;
$end = $to_sum->nelem - 1 if $end >= $to_sum->nelem;
# Perform this thread's sum
$temporary($pid)
.= $to_sum($start:$end)->sum;
});
# This code will not run until that launch has returned
# for all threads, so at this point we can assume the
# workspace memory has been filled.
my $final_sum = retrieve_pdls('workspace')->sum;
print "The sum is $final_sum\n";
As mentioned in the last comment in that example code, the last "parallelize" block will always finish
executing before the next line of Perl code in your script. In other words, all the threads perform a
barrier synchronization just before returning control to your code. Any why would anybody want to force
code to wait at a barrier, you ask?
Nontrivial multi-threaded code must be able to set locations in the code that all threads must reach
before any threads go forward. This is called barrier synchronization and is important when your threaded
code has multiple stages. A particularly important example of an algorithm that needs the ability to set
barrier synchronization points is a time-stepping simulation. In that case, you need to make sure that
all of your threads have a chance to reach the "end" of the time step before moving to the next time
step, since ostensibly the results of one thread's time step depend on the previous results of other
threads. If the calculations for each thread on one step depend on all (or at least some of) the threads
having completed a previous set of calculations, you should use a barrier synchronization event by
calling "parallel_sync".
In light of the synchronization that occurs just before returning control to your code, you can
conceptualize the timeline of your code's execution as follows:
PDL::Parallel::threads::SIMD Execution
======================================
main thread
|
|
|
V
th0 th1 th2 th3 th4 th5 ... th(N-1)
| | | | | | |
| | | | | | |
V V V V V V V
main thread
|
|
|
V
This is in contrast to, say, CUDA, in which a thread-launch returns immediately, thus allowing you to
perform calculations on your CPU while you wait for the GPU to finish:
CUDA Execution
==============
main thread
|
|
|
| --> thread launch
| th0 th1 th2 th3 th4 th5 ... th(N-1)
| | | | | | | |
| | | | | | | |
| V V V V V V V
|
|
|
|
V
It also contrasts with MPI, in which there is no main thread to speak of: all execution occurs in one
thread or another, and any central coordination needs to be specifically orchestrated through a chosen
thread.
It is important to note that if any thread makes a call to "parallel_sync", ALL threads must make a call
to "parallel_sync". Otherwise, the thread will hang until, possibly, the next call for barrier
synchronization, and that could lead to VERY confusing apparent errors in logic. For example:
...
parallelize {
my $pid = parallel_id;
# do some calculations
# Do some thread-specific work
if ($pid < 5) {
# Notice the *two* barriers set up here:
parallel_sync;
# Do something that requires synchronization
parallel_sync;
}
else {
# THIS PART IS NECESSARY TO PREVENT PROBLEMS
# Call parallel_sync the same number of times
# in this else block as in the if block
parallel_sync;
parallel_sync;
}
} 10;
As a general rule, avoid putting "parallel_sync" in conditional blocks like "if" statements. "while"
loops are another possible problem if the condition within the while loop (more specifically, the number
of iterations through the "while" loop) depends on thread-specific aspects of the data. You can do it,
of course, but you have to be very careful that all threads make the same number of calls at the same
algorithmically-intended point in the execution.
FUNCTIONS
This module provides three functions: one for lanching a block of code in parallel across multiple
threads, and two that are meant to be called within that block: a function for synchronizing the
execution of the different threads executing that block and a function to obtain the parallel block's
sequential id.
parallelize
Launches a block of code in parallel across a bunch of threads.
parallelize BLOCK N_THREADS
This function requires two arguments: the block to execute and the number of threads to launch to execute
this block, and returns nothing. This is the means by which you specify the code that you want run in
parallel.
parallel_sync
Synchronizes all threads at the given barrier.
Usage:
parallelize {
# ...
parallel_sync;
# ...
} $N_threads;
This function enforces barrier synchronization among all the threads in your parallelized block. It takes
no arguments and does not return anything.
The barrier synchronization is tightly coupled with the "parallelize" function: you can only call
"parallel_sync" from the middle of a "parallelize" block. If you call "parallel_sync" from outside a
"parallelize" block, you will get an error.
I need to include an example and exposition on when and why to synchronize...
parallel_id
Gives the thread's parallel id.
Usage:
parallelize {
# ...
my $pid = parallel_id;
# ...
} $N_threads;
From within the "parallelize" block, you obtain the current thread's parallel id with this simple
function. When called outside the scope of a "parallelize" block, the function simply croaks.
DIAGNOSTICS
This module does not croak. It does, however, issue a handful of warnings.
"Cannot call parallel_sync outside of a parallelized block"
You tried to issue a barrier synchronization ("parallel_sync") outside the context of a "parallelize"
block, but that's the only context where it makes sense.
"Cannot get parallel_id outside of a parallelized block"
You will get this warning when you ask for a parallel id from code that is not executing in a
parallel block. The resulting return value will be the undefined value.
"Cannot nest parallelized blocks (yet)"
This exception gets thrown when you have a parallelize block within another parallelize block. That's
not presently allowed, though I'm open to any ideas for implementing it if you have any. :-)
"Must request a positive number of parallelized threads"
If you send something that's not a positive integer as the number of threads to launch on your
parallelized block, you will get this error. Always specify a positive integer number.
LIMITATIONS
I am actually quite pleased with how this module has turned out, but there are certainly some
limitations. For example, you cannot launch a parallel block from within another parallel block. You can
still create and join threads, you just cannot do it with the "parallelize" function.
I'm sure there are plenty of limitations, but it's hard for me to see what differentiates a design goal
from a limitation. Feedback on this would be much appreciated.
BUGS
None known at this point.
SEE ALSO
The basic module for Perl parallel computing is threads.
Work on this module was originally inspired by work on PDL::Parallel::threads, so you might want to check
that out.
Modules related to scientific parallel computing include PDL::ParallelCPU, Parallel::MPI,
Parallel::MPI::Simple, PDL::Parallel::MPI and OpenCL.
Other modules provide alternative parallel computing frameworks. These may be less suitable for
scientific computing, but will likely serve other purposes: Parallel::Loops, Gearman,
Parallel::ForkManager, forks, Thread::Pool.
AUTHOR, COPYRIGHT, LICENSE
This module was written by David Mertens. The documentation is copyright (C) David Mertens, 2012. The
source code is copyright (C) Northwestern University, 2012. All rights reserved.
This module is free software; you can redistribute it and/or modify it under the same terms as Perl
itself.
DISCLAIMER OF WARRANTY
Parallel computing is hard to get right, and it can be exacerbated by errors in the underlying software.
Please do not use this software in anything that is mission-critical unless you have tested and verified
it yourself. I cannot guarantee that it will perform perfectly under all loads. I hope this is useful and
I wish you well in your usage thereof, but BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN
WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY
OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE
TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF
THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE
WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
perl v5.40.1 2025-03-27 PDL::Parallel::threads::SIMD(3pm)