Last updated on
Boids, in parallel
In this week’s callback, you will look back at the Boids lab, where you did everything without polymorphism and Scala collections, and change the computation to use Scala collections, and identify opportunities to parallelize the code.
Obtaining the lab files
Run the following commands to obtain a copy of the lab:
$ git clone https://gitlab.epfl.ch/lamp/cs-214/boids-par.git
$ cd boids-par
The provided code contains a possible solution to the previous lab. You will
need to modify the BoidLogic.scala
, World.scala
and HowManyHoursISpentOnThisLab.scala
files to complete this lab.
Step 1: Moving to a new collection
Since our homemade BoidSequence
doesn’t provide a parallel interface yet,
we’re going to have to switch to a different collection.
Start by replacing instances of BoidSequence
with Vector[Boid]
. What other
changes do you have to make to the previous solution now that you have access to
a polymorphic collection?
Step 2: Boids flying out in parallel
With the modified implementation, can you find opportunities for
parallelization? With some careful analysis, you can gain a lot of performance
by adding a .par
and .seq
before and after your operations.
Why do you need both the .par
and the .seq
?
Solution
(v: Vector[Boid]).par
produces a ParVector[Boid]
. Performing map
, etc. on
it will return a parallel vector back where possible. However, since the
external interface expects to see a Vector[Boid]
, and not a ParVector[Boid]
,
you will have to convert the collection back to a Vector[Boid]
. This can be
done with a .seq
or a .toVector
.
val isVector1: Vector[Int] = Vector(1, 2, 3).par // ❌
// Found: scala.collection.parallel.immutable.ParVector[Int]
// Required: Vector[Int]
val isVector2: Vector[Int] = Vector(1, 2, 3).par.seq // ✔️
val isVector3: Vector[Int] = Vector(1, 2, 3).par.toVector // ✔️
Before jumping into the modifications, spend some time analyzing the sequential code. Identify all the places where independent calculations are being made one after the other.
It is most efficient to start from the “top” of your computation. The larger the computation you can split up, the better! This is akin to dividing a big project into its major components first, rather than starting with the tiny details. By splitting the largest independent chunks of the computation, we can maximize the amount of work done in parallel with the least number of threads created.
Hint
You can parallelize tickWorld
as the force on one boid is independent of the
force on another boid in a given step.
Remember, not all parts of the code might be suitable for parallelization. Sometimes, the overhead of parallel execution might outweigh its benefits. Test, test, and test your code again!
Testing
As changing the representation of the data should not affect the behavior of the program, all the previous test suites should continue to pass.
You can run all suites as usual with:
$ sbt test
To allow you to test the performance of your code as you play with it, we have
included two benchmarks: one calls tickWorld
directly, and the other one has a
copy of the sequential code, as before:
/** Class for adding JMH benchmarks */
class Benchmarks:
@Benchmark
def parallelBenchmark =
tickWorld(Benchmarks.random1k, Physics.default)
@Benchmark
def sequentialBenchmark =
// Warning: this sequential baseline is only valid if tickWorld is the only
// parallel function. If tickWorldSequential calls other functions and you
// make those parallel, then they will be called (in parallelized form) and
// benchmarked from here as well!
tickWorldSequential(Benchmarks.random1k, Physics.default)
src/main/scala/boids/Benchmarks.scala
You can run it using the Jmh/run -f 1
sbt command.
-f 1
asks the benchmarking framework, JMH, to run the whole benchmark suite
for only one iteration (this is just to make it faster; you can also pass -w 1 -r 1
to bound the benchmark duration to 1s
). Use -h
for more information.
JMH will report results in the following form (the results below are before parallelization):
[info] Benchmark Mode Cnt Score Error Units
[info] Benchmarks.parallelBenchmark thrpt 5 125.887 ± 6.088 ops/s
[info] Benchmarks.sequentialBenchmark thrpt 5 120.561 ± 13.984 ops/s
The ops/s
number is how many times the benchmark ran per one second (higher is
better). In the boids lab, this is a close proxy for frames per second in the
animation, which is the metric that we care about.
Example results
-
Before converting to
Vector
s and making the code parallel:[info] Benchmark Mode Cnt Score Error Units [info] Benchmarks.parallelBenchmark thrpt 5 125.887 ± 6.088 ops/s [info] Benchmarks.sequentialBenchmark thrpt 5 120.561 ± 13.984 ops/s
-
After (note that the conversion to vector sped up the baseline as well, which is why it’s good to keep both instead of converting and changing to parallelism and comparing that to the BoidSequence baseline):
[info] Benchmark Mode Cnt Score Error Units [info] Benchmarks.parallelBenchmark thrpt 5 456.742 ± 104.352 ops/s [info] Benchmarks.sequentialBenchmark thrpt 5 168.332 ± 11.822 ops/s
You can write your own functions inside this class and tag them @Benchmark
to
test them as well.
Note that adding any non-benchmark members to the Benchmarks
class will
probably result in errors. Use the Benchmarks
object for this.
Submit your patch!
Once your are done, you should submit your code on Moodle. Since we are revisiting old code, rather than writing new code, the submission is done as a patch.
-
If you forgot how to create patches, you can re-watch the online lecture, or redo the introductory exercises on patching.
-
Select files to commit using
git add
. Make sure to select only relevant files. In this assignment, you should be including exactly three files,src/main/scala/boids/BoidLogic.scala
,src/main/scala/boids/World.scala
andsrc/main/HowManyHoursISpentOnThisLab.scala
. -
Save your work using
git add
, followed bygit commit
, with the right parameters. Make sure that your commit message if correctly formatted. We enforce the following format:<Scope>: <Summary> <Description>
- The scope indicates what part of the course the code relates to.
In this assignment, the scope should be
boids-par
. - The summary gives a high-level overview your changes. It should start with a capital letter.
- The description describes the changes in detail—this includes information such as why the changes were necessary, which functions were changed, or what implementation decisions were taken. Here are some examples.
- The complete header line (scope + summary) should be at most 80 characters.
- Note that the
<
and>
symbols are often used to show places where you should fill in a value: do not actually write these symbols in your message!
A plain call to
git commit
will open your$GIT_EDITOR
, which lets you input the header line and the body line separately. If you prefer to stay on the command line, you can pass the-m
option multiple times:git commit -m "<Scope>: <Summary>" -m "<Description>"
. - The scope indicates what part of the course the code relates to.
In this assignment, the scope should be
-
Create a patch from your commit, using
git format-patch
with the correct arguments, and rename it tolab.patch
. -
Submit your patch (named
lab.patch
) to Moodle.
If you run into trouble with the grader, do not modify the patch file by hand. Instead, simply use git commit --amend
to fix up your last commit, then regenerate the patch.
If you need help, review the lecture about version control and the corresponding exercise set.