Showing posts with label OpenCL. Show all posts
Showing posts with label OpenCL. Show all posts

Tuesday, December 25, 2012

GPGPU High Performance Computing using OpenCL -- A Uniform and Portable API across Multi-core CPU and Many-core GPGPU

Although this post overlaps with this previous one, it shows helpful diagrams and its approach is more systematic. Here it is.

1         Multi-Core CPU and Many-Core GPGPU

CPUs began from single-threaded serial processing of general-purpose tasks such as arithmetic, logic and I/O operations.  For many years since then, CPUs improved the performance of single-threaded applications simply by reducing their latencies based on Moore’s Law [1]. Each new CPU provided increased clock frequency, which enabled applications to run faster.  Applications performance was improved by merely porting them to the new CPU even without code modification.
However, every increase in clock speed imposes more demands on power and cooling requirements, and also tends to increase the disparity between CPU and host memory access times.  To counter this, CPUs design employed complex low-level Instruction-Level Parallelism (ILP) [2] such as instruction pipelining, out-of-order execution and speculative execution while still keeping the frequency in economically allowed ranges.  But, efforts to further exploit ILP have stalled since the late-1990s due to the following reasons:
1.       ILP proves inadequate to keep CPU from stalling for host memory accesses because ILP often involves sophisticated hardware design and many applications have difficult-to-predict code;
2.       Many applications need to process independent large data sets;
3.       Many applications need to handle multiple concurrent tasks.
Instead, the industry shifted focus to applying Moore’s Law to implement more cores and larger caches per processor chip.  Larger caches help to reduce the access time gap between CPU and host memory, especially for applications with spatial locality. Cores are units that execute a single stream of program instructions.  A physical core, if it supports core-level multi-threading [3] such as Intel’s HyperThreading, is equivalent to two or more logic ones each of which supports one separate hardware thread (by hardware thread, we mean the execution context (program counters, registers, etc) is maintained on-chip during the entire lifetime of the thread so that context switching between threads can be done in just one cycle and virtually without cost. We will use “core” and “thread” hereafter unless otherwise noted). As you can easily reason, the more cores a system has, the greater parallel computing capacity. This is the case of GPGPU that can easily support tens of thousands threads.
However, these highly parallel computing processes do need us to parallelize application algorithms as much as possible in order to improve performance based on Amdahl’s Law [4]. Algorithm designers employ two commonly used Thread-Level Parallelism (TLP) models [5] to do so. One is Data Parallelism (DP) that decomposes data sets into multiple independent data elements to which a single stream of instructions are concurrently applied.  DP is often used at fine-grained level and supported by a processor’s SIMD function [6].  The other is Task Parallelism (TP) that decomposes applications into many independent tasks that can be concurrently executed across different cores. TP is often used at coarse-grained level and supported by a processor’s MIMD function [6]. TLP improves overall application performance through high throughput of all threads, though latency is actually increased at individual thread levels.
Thanks to the commoditization of many of these parallel processors in the last decade, they are inexpensively available to us as either a workstation or a desktop cluster.  Many large data set processing applications such as CT reconstruction, used to take many hours even several days on traditional serial CPU, can now finish in several minutes on these parallel processors without even resorting to any expensive proprietary hardware such as ASIC, FPGA, Cell BE or multi-node clusters.
Next, we will review two typical parallel processors: multi-core CPU and many-core GPU dedicated to General-Purpose Computing (GPGPU) and how OpenCL provides a uniform parallel computing paradigm for them. We conducted our researches on a Dell Precision T7500 Tower Workstation [7]. It features two Intel Xeon quad-core E5507 CPUs, and one Nvidia Tesla C2050 GPGPU.  Because both processors represent relatively latest developments on parallel computing on CPU and GPGPU, our discussions are around them without loss of generality.

1.1       Multi-Core CPU

From the first 32-bit Intel Core architecture introduced in Jan 2006 up to the latest 64-bit Intel  Xeon  octo-core architecture [8], all multi-core CPUs share the same principle that it is easier to improve overall application performance using throughput-oriented multiple cores than using a latency-oriented single core.  However CPU’s primary role as a general-purpose engine determines trade-off must be made among flow controls, caches and number of cores.
The 64-bit Xeon processors, as shown in Figure 1, represent Intel’s latest developments on multi-core and multi-processor. They target the server and workstation marets. Here are the highlights:
  1. Each processor supports a maximum of 4 threads. HyperThreading is not supported;
  2. Large on-chip L1,L2 and L3 caches (a total of 5.25M) and flow control (not shown in Figure 1);
  3. Each processor supports 3 parallel memory channels that are totally 192-bit wide;
  4. Each processor’s memory bandwidth is 19.2GB/s and peek single floating-point capability is about 36GFLOP/s;
  5. Each core supports 128-bit SIMD vectors through SSE; MIMD is supported at processor level through the 4 cores;
  6. NUMA supports – each processor accesses its locally attached host memory through the integrated memory controller much faster than access other remote host memory attached to the other processor through Quick Path Interconnect (QPI).

1.2       Many-Core GPGPU


From the first GPU invented in 1999 by Nvidia to process graphics only up to its latest Fermi-based Tesla products dedicated to general purpose computing [9] [10], GPU has evolved into a massively parallel processor that can run tens of thousands of threads on its hundreds of cores.
C2050, as shown in Figure 2, turns PCs and workstations into affordable small clusters. Here are the highlights:
  1. Each Streaming Multiprocessor (SM) schedules parallel threads in groups of 32 called warps (AMD’s FireStream GPGPU, C2050’s equivalent, uses wavefronts for the same purpose. So we will use warps hereafter without loss of generosity) to 32 scalar cores.  In other words, each SM maintains its own single stream of instructions of the kernel and each instruction is scheduled to 32 cores on different data in lockstep. This is how Nvidia implements SIMD using its so-called SIMT [11]. SM’s SIMD is 1024-bit wide that is much wider than E5507’s. MIMD is supported by scheduling concurrent kernels across the 14 SMs;
  2. The 14 SMs support a maximum of 21,504 (1,536 x 14) active (resident) threads on the 448 (32 x 14) scalar cores thanks to the large amounts of registers. These numbers of threads and cores are much larger than CPU’s; 
  3. Small on-chip caches (the total is about 1MB) and simple flow control (not shown in Figure 2), which means higher latency than CPU; 
  4. Each SM support 48KB shared memory whose access time is between that of cache and global memory.  This shared memory is explicitly available to programmers through API (on the other hand, caches are transparent to programmers). CPU doesn’t have such memory; 
  5.  All SMs access the off-chip global memory through a wide 384-bit interface that has 6 parallel channels; 
  6. The device connects to the host through a PCIe x 16 Gen 2 slot. Its bandwidth is 8GB/s that is much slower than its 144GB/s memory bandwidth.  The device’s peek single floating-point capability is about 1TFLOP/s that is much larger than E5507’s.

 

1.3       Different Workload Processing on CPU and GPGPU

Because today’s computer systems often form heterogeneous parallel computing ecosystems with CPUs, GPGPUs and / or other types of processors as shown in Figure 3 (T7500 ecosystem hereafter),  it is important to know the type of workloads that each processor does best so that you can partition workloads and schedule them to appropriate processors using OpenCL. By doing so, you can access all the computing power (serial and parallel) of your systems, achieving very high system utilization.


  1.  GPGPU is suited to process large date sets while CPU is better suited to low parallelism (also serial operations such as disk and network IOs, and hard-to-parallelized programs).
    This is because small data sets can’t justify the time incurred by GPGPU’s slow data transfer on PCIe and other setups. Large data sets also spawn a large number of threads that are required in order to take full advantage of GPGPU’s massively parallel structure and to hide its high latency on memory accesses and other operations; 
  2. GPGPU is suited to algorithms with high arithmetic density (the number of arithmetic operations per memory access).
    This is also to hide GPGPU’s high latency. When a warp is stalled due to latency, SM swaps in a ready warp for execution, if any, through its fast context switch to maximize GPGPU utilization.  Latency can be fully hidden if SM always has some arithmetic instructions to issue for some warp at every clock cycle during that latency period.  Obviously the more arithmetic instructions and threads, the more latency is hidden; 
  3. Data transfer on PCIe should be minimized due to PCIe’s slowness.
    In order to keep a high number of operations performed on GPGPU per data element transferred, you sometimes have to move more parts of your program to GPGPU even those parts perform faster on CPU; 
  4. GPGPU is suited to programs whose memory accesses have spatial locality.
    Because off-chip memory accesses have the highest latency, both CPU and GPGPU access a block of consecutive memory such as 32 bytes instead of just the requested single data such as a 4-byte integer. However, GPGPU further coalesces memory accesses by a warp into as few memory transactions as possible.  Better spatial locality means fewer memory transactions, higher cache hit rates and less waste of bandwidth. This is more important to GPGPU than to CPU because GPGPU has smaller caches and more threads; 
  5. GPGPU is suited to programs whose control flow logic is simple.
    This is because SM schedules a common instruction to a warp at a time. Full efficiency is realized when all 32 threads of a warp agree on their execution path. If threads of a warp diverge via a data-dependent conditional branch, the warp serially executes each branch path taken, disabling threads that are not on that path, and when all paths complete, the threads converge back to the same execution path. So divergence leaves SM underutilized.  This is also because CPU has more complex control flow logic and ILP mechanisms.

2         OpenCL – A Better Parallel Computing Paradigm

Parallel computing does need users to parallelize their algorithms using DP and / or TP of some programming paradigm.  However, parallel computing for heterogeneous processors is challenging as traditional programming paradigms for CPU and GPU are very different.
For example, there are Pthreads and OpenMP for CPU, and they all assume a single shared address space and require users to possess complex multi-threading skills.  GPGPU programming originally required users to possess intimate knowledge of graphics APIs (OpenGL or DirectX) and GPU architecture and to make their non-graphics applications look like graphics ones by mapping them into problems that drew triangles and polygons. This placed a great constraint for non-graphics domain users. This old approach was adopted by Fang et al in their CT reconstruction algorithm [12].
With the advent of Unified Shaders,  GPU dedicated to general purpose computing (GPGPU) such as the Tesla series from Nvidia and the FireStream series from AMD, and programming paradigms such as CUDA from Nvidia and APP from AMD, CPU-based non-graphics applications can now directly access the tremendous performance of GPGPU without the above limitations.
Unfortunately all of the above different programming paradigms have very sharp learning curves (it should be very formidable for a non-computer domain expert to program all the parallel processors in the T7500 ecosystem using these paradigms) and some are vendor-specific. Worse yet, none of them may be supported by future parallel processors.
OpenCL [13] is an open industry standard for general purpose parallel programming across CPU, GPGPU, and other processors. From the software perspective, it is a framework consisting of an API to create host programs to coordinate parallel computing across heterogeneous processors, and a subset of ISO C99 with parallelism extensions to create kernels. OpenCL supports both DP and TP without users even knowing complex multi-threading mechanism. This, along with its support of universal C language, can greatly shorten the learning curve of non-computer domain scientists and engineers.
From the hardware perspective, OpenCL enables users to design portable, efficient and high-performance applications through its low-level, close-to-metal abstractions over heterogeneous processors (Although many details of the underlying hardware are exposed, OpenCL can’t expose all details. Otherwise OpenCL’s portability and programmability will be restricted).
In the following sub sections, we introduce these abstractions by borrowing some figures from OpenCL 1.0 specification [13], and mapping OpenCL concepts to the hardware in the Dell T7500 workstation. Readers should be able to figure out the function of each OpenCL concept based on the mapping and our previous introduction on CPU and GPGPU.

2.1       Platform Abstraction


Table 1 shows how the various components in Figure 4 map to CPU and GPGPU.

E5507 CPU
C2050 GPGPU
Compute Device
the CPU chip
the GPGPU chip
Compute Unit (CU)
the 8 cores
the 14 SMs
Processing Element (PE)
the 8 cores
the 32 cores in each SM
Host
Dell T7500 workstation
Dell T7500 workstation
Table 1: Platform Mapping
OpenCL provides API to query all CUs connected to the host and to submit parallel computations on PEs.

2.2       Execution Abstraction

An OpenCL program consists of two parts: kernels that execute on one or more devices and a host program that executes on the host.  OpenCL defines an index space called NDRange (the grid on CUDA) on devices. An NDRange can have 1 to 3 dimensions.  Each point in the index space is called a work-item.  When the host program submits a kernel to a device, an instance of the kernel (a thread) executes for each work-item.
Work-items are organized into work-groups at coarse-grained level, each of which is scheduled to one CU independently. Because C2050’s smallest execution unit is a warp, the work-group size should be a multiple of 32 for best performance. At fine-grained level, each work-term in a work-group can synchronize with each other on shared data accesses.  Work-groups often work with local memory (defined in the next section) on GPGPU to eliminate redundant and stride access on global memory if your program possesses either of them and has spatial locality.
DP is supported by mapping each work-item to one or more data element(s). In strict DP, the mapping is one-to-one. OpenCL implements a relax version of DP where a strict one-to-one mapping is not required.  You should determine the dimension size and mapping based on your program logic and the device’s capability.  TP is supported by submitting multiple task kernels across CUs. Each of these kernels equivalently has only one work- item, which is wasting 31 cores on each SM of C2050. So TP is usually used on CPU each core of which executes one task kernel.
This execution abstraction is scalable because DP work-groups and TP task kernels can automatically and independently be scheduled to any available CU as the host scales to more CUs.

2.3       Memory Abstraction

Because C2050 caches global memory accesses into its L1 and L2 cache, the performance of applications whose memory accesses are not predicable (or possess poor spatial locality) can still be improved.
Because the on-chip local memory’s access time on GPGPU is close to caches, it should be used if a work-group’s spatial locality can be accommodated in it and the work-group items have redundant and /or stride access on global memory. 
Both CPU and GPGPU have registers that consume zero extra clock cycles per instruction. OpenCL doesn’t explicitly expose registers. Instead kernel compilers usually optimize automatic variables to use registers. However if there are more automatic variable than available, these extra automatic variables will be place in the off-chip private memory and this is known as register spilling.  The register usage usually has a significant impact on performance.
Since the constant cache on GPGPU is separate from the private and global caches in L1 and L2, it is not tainted by them. So it can improve performance if you can put constant data there, especially when your kernel doesn’t have good spatial locality.


E5507 CPU
C2050 GPGPU
Global Memory
The off-chip host memory
The off-chip global memory
Local Memory
implemented in host memory
The on-chip shared memory
Private Memory
implemented in host memory
The off-chip local memory
Constant Memory
implemented in host memory
implemented in global memory
Global Memory Cache
implemented in L1, L2 and L3 caches
Implemented in L1 and L2 caches
Constant Memory Cache
implemented in L1, L2 and L3 caches
The constant cache

Table 2:
Memory Mapping


Saturday, February 26, 2011

GPGPU High Performance Computing using OpenCL -- Parallel Computing on Heterogeneous Platforms

The following technical discussion was co-authored with Lili Zhou who is a medical physics resident at NY Presbyterian Hospital / Weill Cornel Medical Center.
The discussion can help you understand the major hardware differences between multi-core CPUs and many-core GPUs (GPGPUs) and the different types of parallel workloads that CPUs and GPUs are each suited for.
Here it is.


Processing of large data sets entails parallel computing in order to achieve high throughput. Traditionally high throughput was only achievable through either proprietary hardware (Application-Specific Integrated Circuit (ASIC), Field-Programmable Gate Array (FPGA) or even Cell BE) or multi-node clusters.
However, as throughput-oriented multi-core processors become more pervasive, the same performance is now achievable through these commodity parallel architectures that range from multi-core CPU for low parallelism to many-core Graphics Processing Unit (GPU) for massive parallelism.
This article focuses on major hardware differences between CPU and GPU, which further decides the different workloads that each processor is suited for. In the end, it gives a data partition example using OpenCL across CPU and GPU, and compares the performance differences on the two platforms.

1 OpenCL – a New and Better Parallel Computing Paradigm

Because you most probably have different workloads that can be better processed using CPU than GPU or vice versa, you need to master parallel computing for both CPU and GPU.
However, parallel computing for heterogeneous processors is challenging as traditional programming paradigms for CPU and GPU are very different.
For example, you usually use POSIX Threads or Pthreads or OpenMP for CPU.
Although GPU typically only handles graphics computations, its gigaflops or even teraflops of floating point performance and high memory bandwidth make it suited to some non-graphics applications including data-intensive scientific and engineering computations, which actually falls into the scope of so-called General-Purpose Computing on Graphics Processing Unit (GPGPU).
However, GPGPU programming originally required programmers to possess intimate knowledge of graphics APIs (OpenGL or DirectX) and GPU architecture and to make their non-graphics applications look like graphics ones by mapping them into problems that drew triangles and polygons. This placed a great constraint for non-graphics domain scientists and engineers.
This old approach was adopted by Fang et al in the CT reconstruction algorithm.
With the advent of Unified Shaders, dedicated general purpose GPU such as the Telsa series from NVIDIA and the FireStream series from AMD, and high-level programming paradigms such as CUDA from NVIDIA and ATI Stream from AMD, CPU-based non-graphics applications can now directly access the tremendous performance of GPU without the above limitations.
Unfortunately all of the above different programming paradigms have very sharp learning curves and some are vendor-specific. Worse yet, none of them may be supported by future parallel processors.
OpenCL (Open Computing Language)
is an open royalty-free standard for general purpose parallel programming across CPU, GPGPU, and such other processors as DSP and the Cell/B.E. processors, giving software developers portable and efficient access to the power of these heterogeneous processing platforms.
Although OpenCL is very new, it is a better choice because of the following factors:
  • It utilizes a subset of ISO C99 with extensions for parallelism. You are probably already familiar with C programming;
  •  You don’t need to learn anything else in order to take advantage of the parallel structures of current and even future parallel processors;
  •  You just need to analyze your algorithms for data or tasks decomposition without even knowing the underlying complex multi-threaded programming mechanism, not to mention any graphics API or GPU internals.
We will use OpenCL terminologies in the following discussion. If you are not familiar with them, you are encouraged to read the first three chapters in the OpenCL specification documentation.

2 Major Differences between CPU and GPGPU in Parallel Computing

You need to know the major hardware differences to be able to select either CPU or GPGPU for different workloads. This discussion uses Dell Precision T7500 Tower Workstation (it has 2 Intel E5507 quad-core processors without Hyper-threading) and NVIDIA Tesla C2050 for reference. You can click their hyperlinks to get introductions for their capabilities. 
C2050 is based on NVIDIA’s next-generation CUDA architecture codenamed “Fermi” and has 14 CUDA 32-core multi-processors one of which is shown below courtesy of this resource:

2.1 GPGPU Supports Much More Hardware Threads than CP

T7500 only supports 8 hardware threads (4 compute units or core in CPU terms * 2 processors) while C2050 supports 21,504 (14 compute units or multi-processors in CUDA terms * 1536), which means C2050 is in a better position to process massive amounts of data in parallel than T7500.

2.2 GPGPU’s Threads Are Lightweight

By the above “hardware threads”, we mean the context or state of a processing element (one of the 4 compute units in T7500 or one of the 32 CUDA core in a C2050 compute unit) is saved in registers allocated to each thread so that context switching can be done in just one cycle.
If you create more than 8 threads for T7500, context switching will involve software operation, which takes many more cycles and are accordingly expensive and heavyweight.
Because C2050 has 32,768 registers, it can support as many as 21,504 in-flight hardware threads.

2.3 GPGPU’s SIMD and Global Memory Interface Is Much Wider than CPU’s

Single-Instruction Multiple-Data or SIMD is suited to process large data sets in batch mode.  T7500 implements SIMD with SSE that is 128-bit wide while C2050 implements SIMD with Single-Instruction Multiple-Thread or SIMT (a smallest execution unit of 32 threads runs a single instruction) that is 1024-bit wide.
T7500 has 192-bit global memory (system memory in CPU terms) interface through 3 parallel channels per processor while C2050’s memory interface is 384-bit through 6 channels.
This means C2050 is better suited to large data sets that possess spatial locality.

2.4 CPU Has Larger Cache and Lower Latency than GPGPU

T7500 has 32K L1 and 256K L2 per compute unit and 4M unified L3 while C2050 has 16K L1 per compute unit and 768K unified L2, which means that CPU is more lenient to the requirement on spatial locality than GPGPU and also has lower memory access latency than GPGPU.
However GPGPU is optimized for high-throughput by hiding the latency for memory access and other instructions through fast context switching to ready threads in the scheduling queue, which assumes, in addition to there being a large number of in-flight thread, the number of arithmetic operations per memory operation is also high (Imagine if you only perform one arithmetic per memory load, context switching doesn’t make much sense).

2.5 GPGPU Gives Developers Direct control over Its Fast On-chip Local Memory

GPGPU’s local memory (shared memory in CUDA terms) approximately corresponds to CPU’s L1 cache with respect to latency.
This memory region is shared by all work items (parallel threads) in a work group (a thread block), and can be used to boost effective bandwidth and system scalability by reducing or eliminating redundant loads from global memory and coalescing stride accesses to global memory.
The coalescing of strides is possible only when memory accesses of all work items in a work group fall into the local memory. In other words, a work group exhibits spatial locality in the local memory.
C2050 has 48K local memory. Without using it, the redundant global memory loads from tens of thousands of threads if they do exist, will cause too many memory bus contentions, and un-coalesced stride accesses will waste too much memory bandwidth.

2.6 Data Needs Transfer Between GPGPU and Host

T7500 uses Gen2 PCIe X 16 for data transfer. Although the bandwidth of Gen2 PCIe X 16 is 8GBps, it is much lower than C2050’s memory bandwidth 144GBps.
So the number of compute operations per data element transferred in your parallel algorithms should be high otherwise they might be better handled using CPU.

3 Different Workload Processing for CPU and GPGPU

A fundamental requirement for parallel computing on both CPU and GPGPU is that you data or tasks can be decomposed for concurrent processing. However CPU and GPGPU are suited to different types of workloads.
On the other hand, if your data or tasks exhibit little or no parallelism, you should leave them to CPU for serial processing. OpenCL allows you to parallelize parts of your CPU-based applications by putting the parallel parts into an OpenCL kernel while leaving the rest untouched.

3.1 GPGPU Is Better Suited to Process Large Data Sets

Small data sets can’t justify the time incurred by GPGPU’s slow data transfer on PCIe and other setup costs. Large data sets also spawn a large number of lightweight threads that are required in order to take full advantage of GPGPU’s massively parallel structure and to hide GPGPU’s the latency for memory access and other instructions by fast context switching.

3.2 GPGPU Requires the Number of Arithmetic Operations per Memory Operation Be High

This is to hide GPGPU’s long latency for memory access and other instructions. Otherwise CPU is a better choice thanks to its larger cache.

3.3 Data Transfer on PCIe Should Be Minimized

In other words the number of compute operations by GPGPU per data element should be high otherwise CPU is a better choice due to the low PCIe bandwidth.

3.4 Memory Access Should Have Spatial Locality

This appears more important to GPGPU than CPU because CPU has larger cache and GPGPU’s wider SIMD and memory interface otherwise only means waste of bandwidth and limited scalability across its compute units.
Please note that you can relax this requirement a bit for GPGPU if your parallel algorithm meets the above 3.2 very well because a large number of arithmetic operations do hide memory access latency. Actually this relaxation is also a winning point because in order to use CPU’s SIMD feature, you must explicitly pack your data into vectors, which requires your data to have perfect spatial locality.

4 Partition Your Data for Data Parallelism

The key to OpenCL programming is to partition your data or tasks so that they can be processed in parallel. Task parallelism needs to vectorize your data and / or to submit multiple small kernels.
Since you face data parallelism most of the time and it needs more data design consideration, this discussion will only deal with data parallelism.
Data parallelism
allows you to concurrently apply the same OpenCL kernel (the function to run on GPU in parallel) to multiple independent work items (data elements) in an OpenCL index space (a memory object decomposed into data elements).  OpenCL doesn’t require you to have a one-to-one mapping between work items and data elements.
When you design a new parallel algorithm, you should put the requirements in Section 3 into consideration. A good candidate for parallelism is the FOR or WHILE loops with large iterations, and large data arrays.
One loop or one array can correspond to one dimension in an OpenCL index space while the loop iterations and array elements can map to work items. Nested loops or multi-dimension arrays can correspond to up to three dimensions in an OpenCL index space.
When you try to parallel an existing singled-threaded algorithm, you should also target its loops and data arrays and analyze its data access pattern to decide which processor is better suited, CPU or GPGPU?
If you are not sure about the selection, which is especially true when your existing algorithm doesn’t fit the requirements in Section 3 one way or the other (for example, the algorithm doesn’t possess too much spatial locality),  you can first parallelize it to CPU. Then you can enhance it to GPGPU with slight modifications (still remember that OpenCL is a general purpose parallel programming paradigm across CPU, GPGPU and other processors?) and compare them.
This approach is what we did for our CT image reconstruction program – Conebeam.
The Conebeam runs on Dell Precision T7500 Tower Workstation equipped with a NVIDIA Tesla C2050.  It has one one-dimensional input array, one one-dimensional output array, and three nested FOR loops that result in about 100M final iterations. The three FOR loops dynamically calculate the indexes for both arrays and compute the output array values based on the input array.
There are more than 200 arithmetic operations per memory access. Only the input and output arrays need transferring to and from GPGPU, respectively.
Here is pseudo-code method to parallelize on GPU:
01.void someMethod(float *inBuf, float *outBuf) {
02.    for(int angle=0; angle
03.      for (int binX=0; binX
04.        for (int binY=0; binY
05.          //the number of final iterations here is about 100M
06.          int idxIn = calculate based on angle,binX and binY;
07.          int idxOut = calculate based on angle,binX and binY;
08. 
09.          outBuf[idxOut] = calculate based on inBuf[idxIn];
10.        }
11.      }
12.    }
13.}
Because the three FOR loops calculate the values of the output array independently, they can be parallelized as the three dimensions in an OpenCL index space.
Because the three FOR loops have large iterations, and the two arrays are also large, this program seems to be a good candidate for GPGPU.
However, because the index accessing order of the two arrays is not synchronous with the three FOR loop iteration order, this program has very bad spatial locality and local memory can’t be used. Fortunately because it has high arithmetic operations per memory access, it should hide memory access latency in some degree if we use GPGPU.
Before we compare the two OpenCL implementations with real tests, here is some estimation:
Since T7500 has 8 compute units running at 2.2GHz while C2050 has 16 compute units running at 1.1GHz, T7500 has an approximate equivalent of 14 C2050 compute units.
T7500 has 192-bit global memory interface through 3 parallel channels per processor, so its overall memory interface is 384-bit. C2050’s memory interface is also 384-bit through 6 channels. 
The significant differences include (a) T7500 has just one processing element per compute unit while C2050 has 32 processing elements per compute unit; (b) T7500 has much larger cache than GPGPU and (c) T7500’s memory bandwidth is 230GBps while C2050 is 144GBps (GPU usually has higher bandwidth than CPU. However this high-end E5507’s effective memory speed is 4.8GHz while C2050 is only 3.0GHz).
The GPGPU version should be approximate 32 times faster than the CPU version if we put aside the cache factor and the CPU version doesn’t use SSE feature (The algorithm’s spatial locality is so bad that the above (c) shouldn’t generate any advantage to the CPU version).
Here are the real testing results:
The original single-threaded version ran about 70 minutes. The CPU version didn’t use SSE in order to port to NVIDIA GPGPU version easily and it ran about 12 minutes. The GPGPU version ran about 1 minute.
The CPU version didn’t have 8 times improvement over the single-threaded version because the Conebeam doesn’t have spatial locality and the 4 cores per processor incur memory bus contentions.
The GPGPU version didn’t have 32 times improvement over the CPU version because again the Conebeam doesn’t have spatial locality and CPU has much large cache.
In order to have linear scalability on GPGPU, the algorithm should be enhanced to exhibit spatial locality if it is possible.

5 Further Discussion

A major challenge for GPU programming is to preserve GPU performance levels while increasing the generality and expressiveness of application interfaces.
Although OpenCL, CUDA and ATI Stream relieve programmers from learning graphics jargons, they may not allow you to make good use of all GPU resources.  The authors are not sure whether this concern still hold true even with NVIDIA’s Telsa series and AMD’s FireStream series.
While OpenCL provides programming portability, it doesn't necesssarily provide performance portability.

Wednesday, February 2, 2011

GPGPU High Performance Computing using OpenCL -- Work with Multiple Vendor Platforms using ICD

Sometimes you have different workloads that are better handled with different vendor OpenCL implementations.
For example if your data presents access locality or coherence, the number of calculation per data element transferred on PCI  is high and the number of calculation per memory access is also high, you can use NVIDIA's OpenCL implementation on its GPGPU.
On the other hand, if your data doesn't possess all of the characteristic mentioned above, you can use ATI/AMD's OpenCL implementation on CPU so that you at least can take advantage of all your CPU cores.

OpenCL has an extension "cl_khr_icd". It is technically called OpenCL ICD (Installable Client Driver) that is a means of allowing multiple OpenCL implementations to co-exist and applications to select between them at runtime.

In my case since I have both CPU and GPGPU-friendly data, I installed on a 64-bit Windows 7 workstation both NVIDIA GPU Computing SDK 3.2 that support OpenCL 1.0, and ATI Stream SDK 2.2 that support OpenCL 1.1.

1. Find out whether your Vendor's SDK Supports OpenCL ICD
You can compile and run NVIDIA's oclDeviceQuery program. If you see "cl_khr_icd" in section "CL_DEVICE_EXTENSIONS" it means NVIDIA supports Opencl ICD.
You can also run ATI's "clinfo" program. It actually prints out all OpenCL platforms. Again if you see "cl_khr_icd" in section "Extension" it means the corresponding platform supports Opencl ICD.

2. How does ICD Work?
Although I setup all my Visual Studio projects with NVIDIA GPU Computing SD, I basically can still dynamically load ATI's CPU OpenCL implementation.
The project setup basically needs two configurations. One is to specify the OpenCL include directory that contains vendor-neutral headers such as cl.h, cl_ext.h and some vendor-specific headers such as cl_agent_amd.h for AMD.
Unless your SDK's have different OpenCL versions, all vendor-neutral headers should be the same. If you need vendor-specific headers, of course you have to setup your project with that vendor's SDK.

The other configuration is to specify the OpenCL library directory where a vendor-neutral stub library "OpenCL.lib" is located. This "OpenCL.lib" is called ICD loader that is responsible to dynamically load a vendor specific ICD library.
When you install a OpenCL SDK, the SDK registers its ICD DLL in the register key
HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenCL\Vendors on Windows if it supports ICD.
So the ICD loader can scan the above register key and dynamically load your vendor's DLL library.

For more information, please refer to this resource. 

3. How does ICD affect your Code?
Your application is now responsible for selecting which of the OpenCL platforms present on a system it wishes to use, instead of just requesting the system default.
This means using the clGetPlatformIDs() and clGetPlatformInfo() functions to examine the list of available OpenCL implementations and selecting the one that best suits your requirements.

For example, previously you could call the following function without specifying the specific platform value:
context = clCreateContextFromType(
     0, CL_DEVICE_TYPE_GPU, NULL, NULL, &status);

With ICD, you must specify a specific platform:

cl_context_properties cps[3] = {
              CL_CONTEXT_PLATFORM,
              (cl_context_properties)platform,
              0};
context = clCreateContextFromType(
     cps, CL_DEVICE_TYPE_GPU, NULL, NULL, &status);