OpenCL: .NET, C# and Resolver One integration -- the very beginnings

Posted on 18 March 2010 in GPU Computing, Programming, Python, Resolver One

Today I wrote the code required to call part of the OpenCL API from Resolver One; just one function so far, and all it does is get some information about your hardware setup, but it was great to get it working. There are already .NET bindings for OpenCL, but I felt that it was worthwhile reinventing the wheel -- largely as a way of making sure I understood every spoke, but also because I wanted the simplest possible API, with no extra code to make it more .NETty. It should also work as an example of how you can integrate a C library into a .NET/IronPython application like Resolver One.

I'll be documenting the whole thing when it's a bit more finished, but if you want to try out the work in progress, and are willing to build the interop code, here's how:

That should be it! If you want to look at the code, the only important bit is in DotNetOpenCL.cs -- and it's simply an external method definition... the tricky bit was in working out which OpenCL function to write an external definition for, and what that definition should look like.

I've put a slightly tidied version of the notes I kept as I implemented this below, for posterity's sake; if you're interested in finding out how the implementation went, read on...

My aim for the day was to successfully call at least one OpenCL function from a Resolver One spreadsheet. When I last looked at OpenCL, I'd managed to get the NVIDIA demos compiling and running on my machine, and so the obvious starting point was to port one of these over. The simplest appeared to be oclDeviceQuery, which just queries the OpenCL devices on the computer and prints out their details, so I started by looking at that and trying to work out how it worked.

In the C++ project that NVIDIA supply for this demo, there's just one file: oclDeviceQuery.cpp. It has no explicit library dependencies (or at least none I could find), and depends on a few header files in the directory structure above the project. It looked simple enough, so I decided to go through all of the code and make sure I understood it all.

Stripped of logging, here's what it does:

So, that's what oclDeviceQuery.cpp does. So now I knew that once clGetPlatformIDs is working in Resolver One, the next functions to add will be clGetPlatformInfo, clGetDeviceIDs, and clGetDeviceInfo.

On to getting the integration working for clGetPlatformIDs, then. In order to call this from Resolver One, I need to be able to call it from IronPython. In order to call it from IronPython, I need to call it from .NET, which is likely best-done using P/Invoke, the .NET interface for calling "platform" (ie. non-.NET) functions.

First step is to have somewhere to keep the code. I figured that an open source library would be reasonable, so created the dot-net-opencl project under the Resolver Systems GitHub account, then branched it under my own account to work on it.

Right, step 1: create a C# project to hold the P/Invoke code. I started by kicking off Visual C# 2008 Express, created a project DotNetOpenCL with an appropriate class name (CL for now -- so we'll have CL.clGetDeviceInfo, but that's no huge deal -- it can be tidied later).

Next step: Visual Studio projects generate an awful lot of junk files, and of course have build products -- DLLs and so on -- none of which you would want in the git repository. I added a .gitignore file as per this excellent Stack Overflow page.

The next step: add a first cut of the the clGetPlatformIDs function, just with the ability to get the number of platforms. Following the example of the P/Invoke stuff in Resolver One's testing framework (which uses native functions to move the mouse etc) I wound up with:

[DllImport("OpenCL.dll")]
public static extern int clGetPlatformIDs(uint num_entries, IntPtr platforms, out uint num_platforms);

Note that this is a bit dodgy; platforms should be a cl_platform_id *, where cl_platform_id is a struct _cl_platform_id * -- in other words, instead of an IntPtr it's actually an IntPtrPtr, which type doesn't exist in .NET. However, if all we're getting is the number of platforms, that should be OK for now.

Building gets a "Build succeeded" message. In DotNetOpenCL\DotNetOpenCL\bin\Release, I got a DLL file.

So, the next step: write a little IronPython script to test it.

from os.path import abspath, dirname, join
dllPath = join(dirname(abspath(__file__)), "DotNetOpenCL", "DotNetOpenCL", "bin", "Release")

import sys
sys.path.append(dllPath)

import clr
clr.AddReference("DotNetOpenCL")

from DotNetOpenCL import CL
from System import IntPtr

errorCode, numPlatforms = CL.clGetPlatformIDs(0, IntPtr.Zero)
print "Error code", errorCode
print "Number of platforms", numPlatforms

I ran it, and got:

giles@MRLEE /c/dev/dot-net-opencl
$ ipy test_clGetPlatformIDs.py
Error code 0
Number of platforms 1

This was really really pleasing: the code had called through to OpenCL from IronPython! Next step: how to deal with getting the number of platforms?

With help from this MSDN P/Invoke tutorial, it became obvious that you can tell P/Invoke that you want to pass a function a .NET array which can be treated as a pointer to a C-style array of pointers on the unmanaged side by using the MarshalAs attribute, for which the unmanaged type is specified using the appropriately-named UnmanagedTypeenumeration. It was pretty clear that the correct P/Invoke specification was:

[DllImport("OpenCL.dll")]
public static extern int clGetPlatformIDs(
        uint num_entries,
        [MarshalAs(UnmanagedType.LPArray)] IntPtr[] platforms,
        out uint num_platforms);

(For future reading: I also found this very in-depth page about P/Invoke on the Mono site, which looks like it will be useful later.)

This compiled OK, so I modified my IronPython testing code (with a reminder of how to create C# arrays from IP from Haibo Luo):

from System import Array, IntPtr

errorCode, numPlatforms = CL.clGetPlatformIDs(0, None)
print "Error code", errorCode
print "Number of platforms", numPlatforms

platforms = Array.CreateInstance(IntPtr, numPlatforms)
errorCode, numPlatformsReturned = CL.clGetPlatformIDs(numPlatforms, platforms)
print "Number of platforms returned", numPlatformsReturned
print "Platform array", platforms
for i in range(numPlatformsReturned):
    print "Platform #%s: %s" % (i, platforms[i])

I ran it, and got this:

giles@MRLEE /c/dev/dot-net-opencl
$ ipy test_clGetPlatformIDs.py
Error code 0
Number of platforms 1
Number of platforms returned 1
Platform array Array[IntPtr]((<System.IntPtr object at 0x000000000000002B [12289
]>))
Platform #0: 12289

w00t!

So by this stage I had an IronPython file that was successfully calling OpenCL and getting the list of platform IDs (which appear to be intended to be opaque types) into a .NET array type. The next step was to write a Resolver One spreadsheet that basically just cloned the functionality of the IronPython script. This was trivially easy, of course :-) So you can see the results in the GitHub repo.

That was it for today! Now that I have the set of functions I need clearly defined, and one of them working properly, then the rest should be pretty simple. I'll look at completing the oclDeviceQuery demo in my next OpenCL post.