Test Driven Development in Julia

By: Abel Soares Siqueira

Re-posted from: http://abelsiqueira.github.io/test-driven-development-in-julia/

First, what is Test Driven Development (TDD)?
Well, I’m not an expert, so don’t quote me, but in practice it means that you
develop your code to fulfill tests that you define prior to beginning your work.
You do not define all your tests first, though. You define a single test,
and produce code to pass it. Then you define another code, and produce code to
pass both. And so forth until you complete your specification.

This is good because:

The steps of TDD can be described as from
Wikipedia

  1. Add tests: These should be useful, and should fail.
  2. Run tests: Verify that the test fails. If not, go back to 1.
  3. Write code: Write enough code to pass the test.
  4. Run tests: Verify that all tests pass. If some of the tests fail,
    go back to 3.
  5. Refactor: Now that everything passes, make the code looks nicer. This
    is harder for non-seasoned programmers, becauses it’s vague. Essentially,
    it means removing duplicate code, magic numbers, clarifying names, etc.
  6. Run tests: Again. Should be done during refactoring, to guarantee
    you’re not breaking anything. But just to be very clear: your tests should
    pass at the end of refactoring
    .
  7. Repeat.

This is one way of describing TDD, but there are other. Many others, by the way.
In fact, there are many images describing it, so you can print one and staple it
around.

Julia

First, we are gonna follow the package layout in Julia.
This post mentions it at the end.
Basically, we need

  • Folder PackageName.jl
    • Folder src
      • PackageName.jl
    • Folder test
      • runtests.jl
    • README.md
    • LICENSE.md

In our example, we’re gonna write a program to convert Roman numbers to decimal,
and vice-versa.
This was inspired by this
site
.

Important: You should use git, but I’ll skip it here

Let’s begin writing the outline of the project

mkdir RomanNumerals.jl
cd RomanNumerals
mkdir src test
# File src/RomanNumerals.jl
module RomanNumerals

end
# File test/runtests.jl
using RomanNumerals

include("test_digits.jl")

This defines the building blocks. Note that test_digits.jl does not exist. We’re
gonna create it to test the individuals digits.

Our testing environment will consist of having a terminal open at all
times at the root of this project. Our testing command will be

julia -L src/RomanNumerals.jl test/runtests.jl

There are different ways to issue the same command, but this is locally good.

Tests

Julia comes with a Base.Test package, which is the least you should use.
For all basic things it is enough. It provides the @test macro, which you can
use as

using Base.Test
@test 1 == 1 # This will pass
@test 1 == 0 # This will fail

We’re gonna go a step beyong and use
FactCheck.jl.
This provides more information about the tests.

We’re gonna implement the function roman_to_dec which receives a string with
roman numerals and returns the decimal equivalent of the number.
With FactCheck, our first test will be

# File test/test_digits.jl
using FactCheck

facts("Testing digits") do
  @fact roman_to_dec("I") --> 1
end

When we run our test, we’ll get

Testing digits
  Error :: (line:-1)
    Expression: roman_to_dec("I") --> 1
    UndefVarError: roman_to_dec not defined
      ...

Look, roman_to_dec not defined. Well, let’s define it.

# File src/RomanNumerals.jl
...
export roman_to_dec

function roman_to_dec(s)
end
...

Running again, we get an even better message

Testing digits
  Error :: (line:-1) :: fact was false
    Expression: roman_to_dec("I") --> 1
      Expected: 1
      Occurred: nothing
Out of 1 total fact:
  Failed:   1

Expected 1, nothing ocurred. Well, that’s easy.

# File src/RomanNumerals.jl
...
function roman_to_dec(s)
  return 1
end
...
Testing digits
1 fact verified

Done. We’re successful. Rejoice. Back to work.

We’ve written a test, we’ve tested it, we’ve written code to fix it, we tested
it. Not much to refactor, this is a silly example.

Repeat. Let’s improve the tests.

# File test/test_digits.jl
...
facts("Testing digits") do
  @fact roman_to_dec("I") --> 1
  @fact roman_to_dec("V") --> 5
end

Running, we obtain

Testing digits
  Error :: (line:-1) :: fact was false
    Expression: roman_to_dec("V") --> 5
      Expected: 5
      Occurred: 1
Out of 2 total fact:
  Verified: 1
  Failed:   1

Now, that’s better. Improving the code.

# File src/RomanNumerals.jl
...
function roman_to_dec(s)
  if s == "I"
    return 1
  else
    return 5
  end
end
...

This too will pass. Notice that this example is very silly. It is instructional,
of course. On a real application, you could start with all digits at once, for
instance.

More tests and solutions:

# File test/test_digits.jl
...
facts("Testing digits") do
  @fact roman_to_dec("I") --> 1
  @fact roman_to_dec("V") --> 5
  @fact roman_to_dec("X") --> 10
end
# File src/RomanNumerals.jl
...
function roman_to_dec(s)
  if s == "I"
    return 1
  elseif s == "V"
    return 5
  else
    return 10
  end
end
...

Now we can refactor, because it’s getting very ugly.

# File src/RomanNumerals.jl
...
const digits = Dict("I"=>1, "V"=>5, "X"=>10)

function roman_to_dec(s)
  return digits[s]
end
...

We can also refactor the test.

# File test/test_digits.jl
...
facts("Testing digits") do
  for (digit,value) in [("I",1), ("V",5), ("X",10)]
    @fact roman_to_dec(digit) --> value
  end
end

Test. Now we can add more tests for digits, and it will be much easier (because
it’s refactored) to both create the test and to solve it.

Understanding the logic now, you can add all the rest of the digits at once.
Remember to test before start fixing, even though is very easy now.
This could be a breaking moment on your code. If, when trying to fix it, you
realize it’s not as simple as you expected. Remove the test, and add a smaller
one. At this time it will be very useful to have been using git.

# File test/test_digits.jl
...
facts("Testing digits") do
  for (digit,value) in [("I",1), ("V",5), ("X",10), ("L",50), ("C",100),
      ("D",500), ("M",1000)]
    @fact roman_to_dec(digit) --> value
  end
end
# File src/RomanNumerals.jl
...
const digits = Dict("I"=>1, "V"=>5, "X"=>10, "L"=>50, "C"=>100, "D"=>500,
  "M"=>1000)
...

Next test

We’ve completed a test. Let’s do the next.

# File test/runtests.jl
using RomanNumerals

include("test_digits.jl")
include("test_double_digits.jl")

Double digits are more complex that single digits (by at least at factor of 2?
🙂 ). Let’s break it down using context.

# File test/test_double_digits.jl
using FactCheck

facts("Testing double digits") do
  context("Repeated digits") do
    @fact roman_to_dec("II") --> 2
  end
end

Testing this will fail (as it should), with a KeyError: II not found, because
we’re using the dictionary, and “II” is not in it.

Before reading the solution, try to fix it yourself. There are many ways to do
it.

# File src/RomanNumerals.jl
...
function roman_to_dec(s)
  dec = 0
  for i = 1:length(s)
    dec += digits[s[i:i]]
  end
  return dec
end

This fixes it. Now to refactor. You may have noticed that s[i] does not work
inside digits. That is because julia differentiates characters and single
digits strings (like C, unlike Python). One refactor option is to change the
dictionary to use chars.
Another option is to use a better variable instead of s, since it start to
become a nuisance to read.
Yet another, is to use another way to make the sum.

Since this post explains the usage of TDD, it ends here.
You can continue with this problem until you can make a complete conversor of
roman to decimal.

Transpiling Julia to C – The LLVM-CBackend

static-julia-logo

Julia Computing carried out this work under contract from the Johns Hopkins University Applied Physics Laboratory (JHU APL) for the Federal Aviation Administration (FAA) to support its Airborne Collision Avoidance System X (ACAS X) program. JuliaCon 2015 had a very interesting talk by Robert Moss on this topic.

In my last blog post on the requirements for statically compiling julia, I promised to describe in more detail the ability to transpile Julia code into C output. I would now like to announce the open-source release of an updated LLVM CBackend that is able to handle the broad set of intrinsic operations used by Julia.

Julia already has a backend for compiling to the LLVM IR. The Static and Ahead of Time (AOT) compiled Julia work improved this backend to be able to handle any valid Julia method directly, instead of only those with sufficient type specialization. The updated LLVM-CBE project is then able to map the LLVM IR to the appropriate C construct, using only a limited subset of the C language (at this time, the output is nearly C89 compliant). The final executable then could be compiled by some other C compiler and would not have any dependency on the llvm project.

There are a few caveats to be aware of:

  • The LLVM IR is more expressive than C, so some parts of the output are not compatible with the JIT code. This means that you should not try to use the system image compiled by a C compiler with the JIT enabled. Additionally, C has much more undefined behavior than LLVM or Julia, so some places in the code may be more verbose than you might have expected to force C to give the intended behavior.

  • This is not a true cross-compiler. This is not the fault of the backend, but of the Julia program generator phase which encodes many platform-specific assumptions as it generates function definitions. This is the same situation you would encounter if you were to, for example, run a C file through cpp then try to compile the the output using a cross compiler. Some examples include:
    • OS-specializations
    • Endianness assumptions
    • Word (pointer) size
    • I am hopeful that the first two will be addressable in the future. Indeed, the coreimg.jl / inference.ji file is already able to avoid making any platform-specific assumption other than sizeof(uintptr_t) and can be used cross-compiled in this manner.
  • The resulting binary includes code for every method that was added to the system image, with no tree-shaking. The main reasons for this is that regardless of the contents of the program, the semantics of Julia require that the __init__ functions of each module will be called (with all of their side-effects). And beyond trivial programs, it is computationally infeasible to statically determine which methods might be called without solving the halting problem. It may be possible to statically bound the answer, but I expect that it will be more beneficial to better modularize the way code for Base is loaded.

Notwithstanding those limitations, the Julia compiler backend was previously not capable of handling all constructs. The backend has always been capable of compiling fully type-specialized method definitions to native code. And this turns out to be sufficient for compiling an unspecialized versions of most functions as well, by specifying a less specialized type signature for the lambda to the compiler. This is true since the semantics of a Julia program (notably dispatch) are defined as operating on the runtime types of the arguments. The ability to infer the types of variables and arguments ahead-of-time to unbox them and devirtualize the calls is important as an optimization, but neither optimization impacts the correctness of the code.

There were a few cases where the backend couldn’t handle the generic method signature without additional machinery: static parameters, intrinsics, and comprehensions.

Static method parameters represent a side channel of additional information, computed as a function of the method type signature and the runtime call types. So while f{T}(x::T) = T is a very simple method definition, it required some extra care to compile it correctly for any x. Compiling this generically requires the ability to pass T as an argument. However, the callee knows x and not T so it can’t readily provide this extra information. Usually, the Julia backend knows the method signature type exactly, which allows it to fill in T as a constant during compilation and avoids the issue. For the static compilation case, the method dispatch system needs to instead insert this extra parameter into the call. It does this by adding a hidden argument to the call signature, and then marking the function pointer as needing to be called with this extra argument.

A second challenge is intrinsics. These calls are function-like, but do not have a dynamic behavioral model behind them. The code-generator simply fails if it was not able to statically predict one of these types (it was the job of generic_unbox and auto_unbox to try to avoid reaching this failure situation. In practice, this isn’t usually an issue since the method signatures for these functions in Base have been strongly statically typed. However, to guarantee this can’t happen required an implementation of a generic version of all intrinsic functions that can be called if the static type is not sufficiently well-known. The updated LLVM CBackend even makes direct use of some of these dynamic copies of LLVM’s normal intrinsics to implement support for emulation of i128 operations when they are not natively supported by the compiler (for example, when using MSVC or when not using x64).

Another case is comprehensions. Currently this is an open issue. This could be worked around using the same mechanism that is used to add static parameters. But since this issue affects correctness of more than just statically compiled code, the general fix is being actively worked for the v0.5 release.


CBackend usage

These instructions assume you have built Julia from source. If you want to use it independent of Julia, please see the instructions in the repo README instead.

Overview:

  1. Build julia from source
  2. Install llvm-cbe
  3. Generate julia output .bc file with --compile=all
  4. Convert julia output .bc -> .c and compile

Start by defining the directory of the julia src root folder where you have previously built an (unmodified) version of julia master (>=v0.5-) as an environment variable:

JULIA_ROOT=`pwd`/julia

Install / compile llvm-cbe

# Build LLVM 3.7.1, for example, here we build it in-tree
make -C $JULIA_ROOT/deps compile-llvm LLVM_VER=3.7.1

# installs llvm-cbe to $JULIA_ROOT/deps/build/llvm-3.7.1/build_Release/Release/bin
git clone [email protected]:JuliaComputing/llvm-cbe.git $JULIA_ROOT/deps/srccache/llvm-3.7.1/projects/llvm-cbe
make -C $JULIA_ROOT/deps/build/llvm-3.7.1/build_Release/projects

Generate the statically-compiled Julia object file

Follow the steps in my previous blog post to create a .bc file with the desired content.

Convert to C source file (.c) and build

Running the llvm-cbe binary on this LLVM bitcode file converts it into C program:

$JULIA_ROOT/deps/build/llvm-3.7.0/build_Release/Release/bin/llvm-cbe 
    sys-plus.bc -o sys-plus.cbe.c

This output file then can be integrated into your normal toolchain and should work with your compiler. For gcc and clang, I have tested with the following flags to suppress uninteresting lint flags while demonstrating compliance to the standard:

WARN='-std=c99 -pedantic -Wall -Wextra -Wno-unused-variable -Wno-unused-function -Wno-unused-parameter -Wno-sign-compare -Wno-unused-but-set-variable -Wno-long-long -Wno-invalid-noreturn'

The resulting output than can be used in the place of the default system image (with JIT compilation disabled):

./julia -J <file>.so --compile=no <ARGS>

or it could be linked into a larger embedded application executable and initialized with the embedding api:

jl_options.compile_enabled = JL_OPTIONS_COMPILE_OFF;
jl_options.image_file = argv[0];
julia_init(JL_IMAGE_CWD);

Interfacing with a Xeon Phi via Julia

By: Christopher Rackauckas

Re-posted from: http://www.stochasticlifestyle.com/interfacing-xeon-phi-via-julia/

(Disclaimer: This is not a full-Julia solution for using the Phi, and instead is a tutorial on how to link OpenMP/C code for the Xeon Phi to Julia. There may be a future update where some of these functions are specified in Julia, and Intel’s compilertools.jl looks like a viable solution, but for now it’s not possible.)

Intel’s Xeon Phi has a lot of appeal. It’s an instant cluster in your computer, right? It turns out it’s not quite that easy. For one, the installation process itself is quite tricky, and the device has stringent requirements for motherboard choices. Also, making out at over a taraflop is good, but not quite as high as NVIDIA’s GPU acceleration cards.

However, there are a few big reasons why I think our interest in the Xeon Phi should be renewed. For one, Intel will be releasing its next version Knights Landing in Q3 which promises up to 8 teraflops and 16 GB of RAM. Intel has also been saying that this next platform will be much more user friendly and have improved bandwidth to allow for quicker offloading of data. Lastly, since the Xeon Phi uses X86 cores which one interfaces with via standard tools such as OpenMP and MPI, high performance parallel codes naturally transfer over to the Xeon Phi with little work (if you’ve already parallelized your code). For this reason many major HPCs such as Stampede and SuperMIC have been incorporating a Xeon Phi into every compute node. These details tell us that for high-performance computing using Xeon Phi’s to their full potential is the way forward. I am going to detail some of my advances in interfacing with the Xeon Phi via Julia.

First, let’s talk about automatic offloading

Automatic offloading allows you to offload all of your MKL-calls to the Xeon Phi automatically. This means that if you are doing lots of linear algebra on large matrices, standard operations from BLAS and Linpack like matrix multiplication * will automatically be done on the acceleration card. Details for setting up automatic offload are given by MATLAB. However, automatic offloading is a mixed blessing. First of all, there is no data persistence. If you are repeatedly using the same matrices, like in solving an evolution equation (i.e. parabolic PDE), this adds a large overhead since you’ll be sending that data back and forth every multiplication. Also, one major downside is that it does not apply to vectorized arithmetic such as .*. Sure you could hack it to be matrix multiplication by a sparse diagonal matrix, but these types of hacks really only tend to give you speedups when your vectors are large since you still incur the costs of transferring the arrays every time.

Still, it’s stupid easy to setup. You compile Julia with MKL and and setup a few environment variables and it will do it automatically. Thus you should give this a try first.

Native Execution

You can also compile code to natively execute on the Xeon Phi. However, you need to copy the files (and libraries) over the Phi via ssh and run the job from there. Thus while this is really good for C code, it’s not as easy to use when you wish to control the Phi from the computer itself as a “side job”.

Pragma-assisted Offloading

This is the route we are going to take. Pragmas are a type of syntax from OpenMP where one specifies segments of the code to be parallelized. If you’re familiar with using parallel constructs from MATLAB/Julia like parallel loops, OpenMP’s pragmas are pretty much the C version of that. For the Xeon Phi, there exists extra pragmas telling the Phi to offload. This also allows for data persistence. Lastly, for many C codes parallelized with OpenMP they are just one pragma away from working on the Phi.

Our workflow will be as follows. We will use a driver script from Julia which will set the environment and use Julia’s ccall to call the C-code with the OpenMP pragmas which will perform parallelized function calls. Notice that in this case Julia is just performing the role of glue code. The advantage is that we can prepare the data and plot the results from within Julia. The disadvantage is that we will have to write a lot of C-code. However, I am currently talking with Intel’s Developer Lab on using their CompilerTools.jl to compile Julia functions to be used on the Xeon Phi. When that’s available, I will write a tutorial on how to then replace the core functions from this script with the Julia functions. Then, the only C-code would be the code which starts the parallel loop. Let’s get started.

The Problem

We wish to solve some simple stochastic differential equations via the Euler-Maruyama method. We will specify the stochastic differential equation of the form

dU_{t} = f(U,t)dt + g(U,t)dW_{t}

via functions f and g. In our code we will also allow the ability to have a function for the true solution in order to perform error calculations.

The Julia Code

Again, at this point the Julia code is quite simple because it is simply performing the glue. Let M be the number of simulations of we perform. Set up empty vectors for the values of U and the true solution Utrue at the endpoints. Note that we only will keep the endpoints from each simulation due to memory issues. If we were to keep the full array for thousands (or millions) of runs this would easily be more memory than the Phi could handle (or even a workstation!). We then need to specify our environmental variables. Set OMP_NUM_THREADS to be the number of compute cores on your system. We setup MIC_PREFIX, LD_IBRARY_PATH, and MIC_LD_LIBRARY_PATH so that we can dynamically link to our library. Note that this assumes that you have already sourced the compiler variables via compilervars.sh with the argument intel64. If not, you can use Julia’s run function to source the script. Lastly, we set a constant MIC_OMP_NUM_THREADS to be the number of threads the Xeon Phi will use. Since when offloading you can use all but 1 core (one manages the jobs) and each core has 4 threads, we set 240 threads (for the 5110p). Like in the case of GPUs, using more threads than cores is beneficial since the cores can utilize large vectors to do multiple calculations at once (SIMD) and other magic. We set the environment variable OFFLOAD_REPORT to 3 which will make the Phi give us details about everything it’s offloading (good for debugging). Lastly, we end by calling the library. The total code is as follows:

M = 240000
ENV["OMP_NUM_THREADS"]=12
Us = Vector{Float64}(M)
ts = Vector{Float64}(M)
Ws = Vector{Float64}(M)
Utrues = Vector{Float64}(M)
MIC_OMP_NUM_THREADS = 240
ENV["MIC_PREFIX"]="MIC"
ENV["OFFLOAD_REPORT"]=3
ENV["LD_LIBRARY_PATH"]=string(ENV["LD_LIBRARY_PATH"],":.")
ENV["MIC_LD_LIBRARY_PATH"]=string(ENV["MIC_LD_LIBRARY_PATH"],":.")
alg = 2
#ccall((:monte_carlo,"/home/crackauc/XeonPhiTests/EMtest/sde_solvers_noffload.so"),Void,(Cint,Ptr{Cdouble},Ptr{Cdouble},Ptr{Cdouble}),M,Us,Utrues,ts)
@time ccall((:monte_carlo,"/home/crackauc/XeonPhiTests/EMtest/sde_solvers.so"),Void,(Cint,Ptr{Cdouble},Ptr{Cdouble},Ptr{Cdouble},Ptr{Cdouble},Cint,Cint),M,Us,Utrues,ts,Ws,MIC_OMP_NUM_THREADS,alg)

For more of an explanation on using the ccall function to interface with C-code, see my previous blog post. Note that the arrays Us, Utrues, ts, and Ws will be updated in place as the value of U, Utrue, t, and W at the end of the path. Thus after the job is done one can use Julia to plot the results.

Xeon Phi Driver Function

The ccall function looks for a function of the following type in a shared library named sde_solvers.so:

void monte_carlo(int M,double* Us,double* Utrues,double* ts,double* Ws,const int MIC_OMP_NUM_THREADS,int alg)

In this function we will just do a parallel for loop where each iteration calls the Euler-Maruyama solver on a different random seed. However, instead of doing a straight parallel for loop, we will put a little separation between “the parallel” and “the for” so that we can keep some persistent data to be a little more efficient.

We start by defining some constants:

double Uzero = .5;
  double dt = 0.00001;
  double T = 2.0;
  int N = ceil(T/dt)+1;

Now we send the job over to the Xeon Phi via the following pragma:

#pragma offload target(mic:MIC_DEV) default(none) in(Uzero,dt,T,N,MIC_OMP_NUM_THREADS,alg) out(Us:length(M)) 
  out(Utrues:length(M)) out(ts:length(M)) out(Ws:length(M))

Note that at the top of the script we have

#ifndef MIC_DEV
#define MIC_DEV 0
#endif
 
#include <stdlib.h>
#include <stdio.h>
#include <omp.h>
#include <mathimf.h>
#include "mkl.h"
#include "mkl_vsl.h"

and so MIC_DEV singles out the Xeon Phi labeled 0. Using in we send over the variables, and with out we specify the variables we want the Phi to send back. By adding default(none) we get informed if there are any variables which weren’t specified.

After that pragma, we are on the MIC. The first thing we will do is set the number of threads. I don’t know why but setting the environment variable MIC_OMP_NUM_THREADS in Julia does not set the number of MIC threads, so instead we do it manually on via the command

omp_set_num_threads(MIC_OMP_NUM_THREADS);

Next we start our parallel environment by

#pragma omp parallel default(none) shared(Uzero,alg,dt,T,N,M,ts,Us,Utrues,Ws)

Once again, default(none) will make sure no variables are accidentally set to shared, and we specify all of the inputs as shared. With this, we are now coding with that list of variables on the individual threads of the Phi. Thus we will now will setup an individual run of the SDE solver. We make arrays for time t, the Brownian path W, the solution U, and the true solution Utrue. We also grab the id of the thread to setup random seeds later. This gives:

      int i;
      int tid = omp_get_thread_num();
      double* t; double* W; double* U; double* Utrue;
      int steps;
      t = (double*) malloc(N*sizeof(double));
      W = (double*) malloc(N*sizeof(double));
      U = (double*) malloc(N*sizeof(double));
      Utrue = (double*) malloc(N*sizeof(double));

Now we start our parallel for loop. Notice that by allocating these variables before the loop we have increased our efficiency since each run we will simply write over these values, saving us the time of re-allocating. In our for loop we set the initial values (since we are re-using the same arrays), call the solver algorithm, save the results at the end, and re-run. After we are done with the whole loop, then we free that arrays we made. The code is then as follows:

      #pragma omp for
      for(i=0;i<M;i++){
        t[0]=0;
        U[0]=Uzero;
        Utrue[0]=Uzero;
        W[0] = 0;
        euler_maruyama(&f,&g,&trueSol,Uzero,dt,T,t,&W,U,Utrue,tid*i*M+i); /*unique identifier tid*i*M+i since tid spacing */
        Us[i] = U[N-1];
        Utrues[i] = Utrue[N-1];
        ts[i] = t[N-1];
        Ws[i] = W[N-1];
      }
    free(t); free(Utrue); free(U); free(W); free(Z);

Notice that tid*i*M+i has spacings larger than M and tid and so each value will be unique. This is then the value we can use as a random seed. The full code for the driver function is then:

void monte_carlo(int M,double* Us,double* Utrues,double* ts,double* Ws,const int MIC_OMP_NUM_THREADS,int alg){
  double Uzero = .5;
  double dt = 0.00001;
  double T = 2.0;
  int N = ceil(T/dt)+1;
  #pragma offload target(mic:MIC_DEV) default(none) in(Uzero,dt,T,N,MIC_OMP_NUM_THREADS,alg) out(Us:length(M)) 
  out(Utrues:length(M)) out(ts:length(M)) out(Ws:length(M))
  {
    omp_set_num_threads(MIC_OMP_NUM_THREADS);
    #pragma omp parallel default(none) shared(Uzero,alg,dt,T,N,M,ts,Us,Utrues,Ws)
     {
      int i;
      int tid = omp_get_thread_num();
      double* t; double* W; double* U; double* Utrue;
      int steps;
      t = (double*) malloc(N*sizeof(double));
      W = (double*) malloc(N*sizeof(double));
      U = (double*) malloc(N*sizeof(double));
      Utrue = (double*) malloc(N*sizeof(double));
      #pragma omp for
      for(i=0;i<M;i++){
        t[0]=0;
        U[0]=Uzero;
        Utrue[0]=Uzero;
        W[0] = 0;
        euler_maruyama(&f,&g,&trueSol,Uzero,dt,T,t,&W,U,Utrue,tid*i*M+i); /*unique identifier tid*i*M+i since tid spacing */
        Us[i] = U[N-1];
        Utrues[i] = Utrue[N-1];
        ts[i] = t[N-1];
        Ws[i] = W[N-1];
      }
      free(t); free(Utrue); free(U); free(W); free(Z);
    }
  }
}

Notice I left out the extra algorithms. When I put this in a package (and in my soon to be submitted code for a publication) I have different choices for the solver, but here we will just have Euler-Maruyama.

The Inner Functions

Before we get to the solver, notice that euler_maruyama takes in three functions by handle. However, since these will be executed on the Xeon Phi we decorate them with __attribute__((target(mic))). However, I will leave off these declarations since we can instead have them be put on automatically by a compiler command (and this makes it easier to re-compile to be a Xeon Phi free code). Thus the SDE functions are simply

double f(double t,double x){
  return (1.0/20.0)*x;
}
 
double g(double t,double x){
  return (1.0/10.0)*x;
}
 
double trueSol(double t, double Uzero,double W){
  return Uzero*exp(((1.0/20.0)-((1.0/10.0)*(1.0/10.0))/2.0)*t + (1.0/10.0)*W);
}

Thus the SDE is

dU_{t} = frac{1}{20} U_{t}dt + frac{1}{10} W_{t}

which a mathematician would call Geometric Brownian Motion or what someone in finance would know of as the Black-Scholes equation. Our inner function euler_maruyama is then the standard loop for solving via Euler-Maruyama where we replace any instance of dt with a small real number and we replace dW_{t} with normal random variables with zero mean and variance dt. The only tricky part is getting normal random variables, but I used Intel’s VSL library for generating these. The code for solving the Euler-Maruyama equations are then

void euler_maruyama(double (*f)(double,double),double (*g)(double,double),double (*trueSol)(double,double,double),double Uzero,double dt, double T,double* t,double** W,double* U,double* Utrue,int id){
  int N = ceil(T/dt)+1;
  *W = (double*) malloc(N*sizeof(double));
  VSLStreamStatePtr stream;
  vslNewStream(&stream,VSL_BRNG_MT19937,20+id);
  vdRngGaussian(VSL_RNG_METHOD_GAUSSIAN_BOXMULLER,stream,N,*W,0.0f,1.0f);
  (*W)[0] = 0.0f;
 
  int i;
  double dW;
  double sqdt = sqrt(dt);
  for(i=1;i<N;i++){
    /* dW = 0; */
    dW = sqdt* (*W)[i];
    t[i] = t[i-1] + dt;
    (*W)[i] = (*W)[i-1] + dW;
    U[i] = U[i-1] + dt*f(t[i-1],U[i-1]) + g(t[i-1],U[i-1])*dW;
    Utrue[i] = trueSol(t[i],Uzero,(*W)[i]);
  }
  vslDeleteStream(&stream);
}

Notice that this part is nothing special and quite close to what you’d write in C. However, we do note that since we want the value of W at the end of the run outside of this function, and we allocate W within the function, we have to pass W by reference via &W and thus every time it is used we have to deference it via *W. Other than that there’s nothing fancy here.

Compilation

This is always the hardest part. However, notice that if we just take away the offload pragma this is perfectly good OpenMP code! You can do this from the compiler to first check your code. The compilation command is as follows:

icc -mkl -O3 -openmp -fpic -diag-disable 10397 -no-offload -Wno-unknown-pragmas -std=c99 -qopt-report -qopt-report-phase=vec -shared sde_solvers.c -o sde_solvers.so

Most of it is setting up offload reports and libraries, but the important part to notice is that -no-offload is the part that turns off the offload pragma. Give this a try and it should parallelize on the CPU. Now, to compile for the Phi, we use the command

icc -mkl -O3 -openmp -fpic -diag-disable 10397 -qoffload -Wno-unknown-pragmas -std=c99 -qopt-report -qopt-report-phase=vec -shared sde_solvers.c -offload-attribute-target=mic -o sde_solvers.so

Notice that the command -offload-attribute-target=mic is required if you do not put __attribute__((target(mic))) in front of each function that is called when offloaded. I prefer to not put the extra tags because icc required that I delete them to re-compile for the CPU. In this case, we simply get rid of that compiler directive and change to -no-offload and we have working CPU code. Thus you can see how to transfer back and forth between the two via compilation.

After doing this you should be able to call the code from Julia, have it solve the code on the Phi, and then return the result to Julia.

Future Steps

Notice that the functions f, g, and trueSol are simple functions which we pass by pointer into the solver. Julia already has ways to pass function pointers which I go over in my previous tutorial, though since they are not compiled with the __attribute__((target(mic))) flag they will not work on the Phi. Hopefully Intel’s compilertools.jl will support this in the near future. When that’s the case, these functions could be specified from within Julia to allow us to create libraries where we can use Julia-specified functions as the input.

However, this gives a nice template for performing any kind of Monte Carlo simulation or anything else that uses a parallel for loop. This wrapper will form the basis of a library I am creating for stochastic (partial) differential equations. More on that later. In the meantime, have fun experimenting with the Phi!

The post Interfacing with a Xeon Phi via Julia appeared first on Stochastic Lifestyle.