Thursday, May 6, 2010

Parallel Compilation with OpenCL

If you've ever developed with OpenCL, you may have noticed something quite annoying - the runtime compiler is not thread-safe! In an age of computers with ever more cores and GPUs with two orders of magnitude more cores than that, this is really an embarrassment, especially since the entire point of OpenCL is to take advantage of this parallelism!

However, fear not, for there is a work around to this madness. While the runtime compiler may not be thread safe, it is at least process safe, at least for Nvidia's implementation. It may also be process safe for ATI's implementation as well, but I don't have a card available to test this on.

Since two OpenCL compilers can in fact run in parallel if they're created by different programs, all that needs to be done to exploit this is to simply run a compiler program for each CPU and Boom! Problem solved! Right?

Well, not so fast. As with anything else concerning programming, there are details...

First off, Process.Start() is known to be one of the most expensive calls in the Windows API, taking up to a second to execute! This is because there's all manner of overhead for starting a new process, from security concerns to disk access to read in the binary. Thus, the naive method of starting a new process each time we need to compile something is a bad idea.

Instead, we need persistent compile processes, which continue running throughout the lifespan of the master program. Ideally, the master program would start up the processes, then send them bits of source code as it needed things to be compiled. The slave compile processes would then build the source and send the binaries back to the master program, which would execute them. After some time of doing this, the master program would finish, at which point it would need to not only close itself, but close the slave processes as well. Since the standard kill process command doesn't give the process time to clean up, the master will have to send the slave processes a kill message, so that they can close themselves.

The key to all this is to find an API through which the processes can send messages back and forth asynchronously and without mangling binary data (this rules out stdin/stdout. In addition, it would be nice if the API also worked with MONO.NET, so that the program can be run on other OSes.

One such API is System.IO.Pipes. Here is the messaging system that flam4OCL uses:

Here's the messaging class used by the master process:
module CompilerDriver

open System
open System.IO
open System.IO.Pipes
open System.Text
open System.Diagnostics

type CompilerService (pipe:NamedPipeServerStream)=
let pipe = pipe
let readMessage() =
async {
let! length = (pipe:>Stream).AsyncRead(sizeof)
let buf = Array.create(BitConverter.ToInt32 (length,0)) 0uy
let! msg = (pipe:>Stream).AsyncRead(buf)
return buf
} |> Async.RunSynchronously
let writeMessage (msg:byte[])=
let lengthBuf = BitConverter.GetBytes (msg.Length)
pipe.Write (Array.append lengthBuf msg,0,msg.Length+sizeof)

new(deviceName) =
//ensure a unique pipe name
let pipeName = Guid.NewGuid().ToString()
let pipe = new NamedPipeServerStream(pipeName,PipeDirection.InOut,1,PipeTransmissionMode.Message)
let compilerProcess = new Process()
let startInfo = new ProcessStartInfo()
startInfo.FileName <- @".\Flam4CompileService.exe"
startInfo.Arguments <- pipeName + " " + "\"" + deviceName + "\""
startInfo.UseShellExecute <- false
compilerProcess.StartInfo <- startInfo
compilerProcess.Start() |> ignore
pipe.WaitForConnection()
new CompilerService(pipe)

interface System.IDisposable with
member s.Dispose() =
writeMessage <| Encoding.UTF8.GetBytes("END")
pipe.WaitForPipeDrain()
pipe.Close()
pipe.Dispose()

member s.Compile (kernel:string) =
let buf = Encoding.UTF8.GetBytes(kernel)
writeMessage buf
pipe.WaitForPipeDrain()
readMessage()

And the slave compiler process:
module Program

open System
open System.IO
open System.IO.Pipes
open System.Text
open Cloo

let standardOCLCompilerOptions = @"-cl-single-precision-constant -cl-denorms-are-zero -cl-mad-enable -cl-no-signed-zeros -cl-fast-relaxed-math"

[<STAThread>]
[<EntryPoint>]
let main args =
//Connect to the pipe
use pipe = new NamedPipeClientStream(args.[0])
pipe.Connect()
pipe.ReadMode <- PipeTransmissionMode.Message
try
let readMessage (pipe:NamedPipeClientStream)=
async {
let! length = (pipe:>Stream).AsyncRead(sizeof)
let buf = Array.create(BitConverter.ToInt32 (length,0)) 0uy
let! msg = (pipe:>Stream).AsyncRead(buf)
return buf
} |> Async.RunSynchronously
let writeMessage (pipe:NamedPipeClientStream) (msg:byte[])=
let lengthBuf = BitConverter.GetBytes (msg.Length)
pipe.Write (Array.append lengthBuf msg,0,msg.Length+sizeof)

let msgToStr buffer =
buffer |> Encoding.UTF8.GetString

//set up the OpenCL compiler for the selected device
let clPlatform = ComputePlatform.Platforms.Item(0)
let clContextProps = new ComputeContextPropertyList(clPlatform)
let clDevice = (clPlatform.Devices |>
(fun devs -> [for device in devs -> device]) |>
List.filter (fun device -> device.Name=args.[1])).[0]
let devList = new System.Collections.Generic.List()
devList.Add clDevice
let dev = new System.Collections.ObjectModel.ReadOnlyCollection(devList)
let clContext = new ComputeContext(dev,clContextProps,null,System.IntPtr 0)

//Listen to pipe and compile recieved kernels until the message "END" is recieved
let rec readLoop() =
let msg = readMessage pipe |> msgToStr
if (msg.Length>0)&&(msg<>"END") then
let clProgram = new ComputeProgram(clContext,msg)
try
clProgram.Build(dev,standardOCLCompilerOptions,null,System.IntPtr 0)
with
| :? Cloo.ComputeException ->
printfn "%s" (clProgram.GetBuildLog(clDevice))
| _ ->
reraise()
writeMessage pipe (clProgram.Binaries.Item(0))
pipe.WaitForPipeDrain()
if (msg<>"END") then readLoop() else () //Tailcall to simulate while loop

readLoop()
finally
pipe.Close()
0


Since Wordpress keeps messing with the code format and eating the HTML attributes I insert to preserve the code in a readable, not flowing off the page format, I think it's best that I stop this post here and continue with the description in the next post.

*EDIT* Seems that Wordpress now strips every attribute from HTML, including style="overflow-x:scroll". This makes it impossible to post code without it flowing off the page. I don't have time for this nonsense, so I've moved the blog over to Blogger instead.

No comments:

Post a Comment