Abstract
Nowadays the use of hardware accelerators, such as the graphics processing units or XeonPhi coprocessors, is key in solving computationally costly problems that require high performance computing. However, programming solutions for an efficient deployment for these kind of devices is a very complex task that relies on the manual management of memory transfers and configuration parameters. The programmer has to carry out a deep study of the particular data that needs to be computed at each moment, across different computing platforms, also considering architectural details.
We introduce the controller concept as an abstract entity that allows the programmer to easily manage the communications and kernel launching details on hardware accelerators in a transparent way. This model also provides the possibility of defining and launching central processing unit kernels in multi-core processors with the same abstraction and methodology used for the accelerators. It internally combines different native programming models and technologies to exploit the potential of each kind of device. Additionally, the model also allows the programmer to simplify the proper selection of values for several configuration parameters that can be selected when a kernel is launched. This is done through a qualitative characterization process of the kernel code to be executed.
Finally, we present the implementation of the controller model in a prototype library, together with its application in several case studies. Its use has led to reductions in the development and porting costs, with significantly low overheads in the execution times when compared to manually programmed and optimized solutions which directly use CUDA and OpenMP.
1 Introduction
Currently, the systems used for High Performance Computing (HPC) include accelerator devices. This trend is noticeable from personal computers to classical supercomputers. Examples of these HPC heterogeneous environments can be seen in the first positions of the current top 500 list of supercomputers. When developing solutions to be deployed in heterogeneous systems, we can: (1) Use a single programming model responsible for managing the architectural and conceptual differences between the different computational devices, e.g. OpenACC; or (2) Use a combination of different programming models specific for each kind of computational device, e.g. MPI together with CUDA, OpenCL, or OpenMP.
One of the main drawbacks of the first approach is the difficulty in representing non-complete regular programs, with non-trivial communications or synchronizations. Besides, the final generated code resulting from programming with this kind of model is not usually as well optimized as the original code. For example, the current OpenACC compiler implementations do not offer a complete solution to the problem of automatically choosing appropriate values for several kernel-launching parameters without any programmer guidance, as required by the standard. These parameters include the threadblock size and its multi-dimensional geometry, or the configuration of the sizes of the L1 cache vs. the shared-memory in modern Graphics Processing Units (GPUs). On the other hand, implementing solutions following the second approach requires a deeper knowledge of the different parallel programming models involved, using different synchronization and memory access strategies for different devices. Additionally, the programmer is the final one responsible for properly managing the data transfer among the different memory spaces of the computational devices, at the most appropriate times, together with the choice of proper values for kernel-launching configuration parameters. However, this manual adjustment gives the programmer the possibility to optimize the use of the particular resources of each specific device. Other approaches that try to create a conceptual bridge between these two approaches are domain specific, or are based in sophisticated compiler technology for the generation of coordination or kernel codes.
In this paper, we present the concept of controller as an abstract entity that allows the programmer to transparently manage the launching of series of tasks on accelerator devices, and/or multi-core central processing unit (CPU) processors. It is a redefinition of the Communicator entity presented in Alonso–Mayo et al. (2016), with new functionalities and a completely new internal design that leads to a very efficient implementation. The controller uses the most appropriate programming models (CUDA, OpenMP,…) to exploit the computational resources of the accelerators and host machines. The model has several important features: (1) A mechanism to define common kernels reusable across different types of devices, or specialized kernels for specific kinds of devices, including the possibility to consider a subset of CPU cores as a single independent device; (2) An optimization system to select proper values for kernel-launching configuration parameters (such as the threadblock geometry), guided by simple qualitative code characterization provided by the programmer; (3) A transparent mechanism for memory management, including optimized communications of the data structures between the host and the corresponding images in the accelerators; (4) An abstraction for indexed data structures that unifies the data management in kernels for different kinds of devices (such as GPUs, or multi-threaded vector CPU cores).
This paper also describes a prototype, presented as a C/C++ library extension, that implements the controller entity. The prototype is designed to exploit NVIDIA’s GPUs using the CUDA parallel programming model, or subsets of CPU cores using OpenMP. We also present an experimental study based on several case studies, some of them obtained from the CUDA Toolkit Samples. The study shows how the use of the prototype implies a reduction in the programming effort needed to implement and port these applications, when compared with the original versions that directly use the specific native parallel programming models. Besides, we show that our implementation does not introduce significant performance overheads.
The structure of the article is as follows. In Section 2 we discuss some related work. Section 3 presents the controller model. Section 4 explains the prototype library, its usage to develop a program and relevant implementation details. Section 5 describes the experimental study. Finally, Section 6 describes the conclusions of this work and problems that will be addressed in the future.
2 Related work
In this section, we discuss some work related to our proposal. We cover some examples of different approaches that ease the integrated programming of different computational devices, including accelerators.
There are several works integrating, in the same tool, different parallel languages or models, or considering different device types. One of these models, widely known, is OpenCL. It provides the Context abstraction, defining a memory model in which associated data is shared or moved between the host and the device memories when needed. Although OpenCL is internally exploiting the vendor drivers and native programming tools, its abstractions have been proven to not obtain the same efficiency as that when using the vendor programming models directly, for several common situations (Karimi et al., 2010). Moreover, the implementations are not easily reachable with respect to the definition of the management policies of the internal queues, or the possibility of changing them. Another drawback of this model is the manual management of kernel compilation at run-time, for different architectures in different contexts, that is desired to be generalized and simplified. The llCoMP tool (Reyes and de Sande, 2012) is a source-to-source compiler that translates C annotated code to MPI + OpenMP or CUDA. However, it does not support the joint use of CUDA with the other parallel models. SkelCL (Steuwer and Gorlatch, 2013) enhances the OpenCL interface to allow the coordination of different GPUs on the same machine. The works Chen and Zhang (2009) and Karunadasa and Ranasinghe (2009) introduce hybrid MPI + CUDA approaches to coordinate GPUs in the same or different host machines. Apart from their specific limitations, they do not have an abstract support to homogeneously manage units of different nature, including CPU cores, as in our approach.
There are other approaches that are more domain specific, but include small abstractions to ease the management between the accelerator and the host (e.g. MCUDA (Stratton et al., 2008) or hiCUDA (Han and Abdelrahman, 2009)). They do not consider guided optimizations, nor allow the programmer to control the load distribution or the device coordination. Other approaches with similar limitations also consider CUDA, MPI, and OpenMP combinations (Yang et al., 2011; Howison et al., 2012).
More general approaches propose complete integrated frameworks, such as OMPICUDA (Liang et al., 2012), StarPU (Hugo et al., 2013), or the skeleton programming framework based on it, SkePU (Dastgeer et al., 2011). In general, they hide the coordination details to the programmer, to the point of constraining the potential optimizations that could be achieved manually. The selection of launching parameters, like the threadblock size, is tackled in SkePU, but using a trial-and-error method, thus leaving no possibility to extrapolate the results to other kernel codes or architectures. A higher-level approach is to rely on compiler technology to transparently generate code for different kinds of devices (both for coordination and for kernels) from a single unified language. For example, C++14 is used in PACXX (Haidl and Gorlatch, 2014), a transformation system integrated into the LLVM compiler framework. It transforms explicit parallel constructions that use the concept of kernel and launching in an abstract and elegant form. On the other hand, some of the solutions depend on features of this language, and they are not easily portable to other ones. The decisions about launching parameters, such as the threadblock choice, are still the responsibility of the programmer alone.
Our solution solves previous limitations. It integrates the coordination of computational devices of very different natures in the context of a compiler library, using a new approach based on simple abstractions. It hides the differences of execution models up to the point of allowing the development of generic kernels portable across devices. But at the same time, it allows the integration of native or vendor programming models, applying specific optimization techniques, and avoiding efficiency losses associated with other generic approaches.
Our approach can be also used as a run-time system to generalize the porting of programs across different types of accelerator devices. Several automatic code-generation compile-time tools (Baskaran et al., 2008; Dathathri et al., 2013; Newburn et al., 2013) can derive, from sequential code with pragma annotations: (1) The data dependencies among the kernel launches; (2) The required data transfers between the host and the target devices, and; (3) The domains on which kernels should be executed for accelerators. Such tools have been used typically to generate code for only one kind of accelerator. For example, in Newburn et al. (2013), the OpenMP 4.0 #pragma offload feature is used to generate code to execute on the XeonPhi accelerator. Using the same information extracted by these technologies, together with the abstractions and generic programming guidelines of our proposal, it is possible to generate a generic code valid for any kind of execution device. This approach will be studied in future work.
3 Controller model
The controller model introduces a simplified way to program applications that can exploit heterogeneous computational platforms including accelerators or/and multi-core CPUs. Its architecture is represented in Figure 1. The device controllers coordinate the execution of series of kernels. These kernels are declared as functions, that are managed by the controller entities. Controllers automatically manage the two main concepts used in a program that exploits accelerators.
Kernel management, including the kernel launching and configuration. The controller manages the deployment/execution of sequences of kernel functions in the computational device associated to the controller. The controllers can include policies to exploit concurrent kernel execution techniques, interleave computations with communications, or reorder the sequence of kernels. The kernel configuration is the selection of specific configuration parameters for the kernel launching, that can be associated to a particular kernel and computational platform.
Data management, including the data transfers carried out across the memory hierarchies of the host and the accelerators and the abstraction used to access data elements independently of the target device, the threads indexes space, or the data layout.

Diagram of the controller model architecture. The kernel-launching requests can be enqueued. The controller entity manages the execution of enqueued kernels, and for bounded variables, the data transfers between memory spaces. In the figure, the host variable A is not bounded to the controller. Variable B is a bounded variable, with a duplicated image for the data in the device memory. Variable C is an internal variable of the controller, defined in the host, but allocated only in the device.
3.1 Kernel management
Kernel launching is an operation that inserts an order to execute a kernel in the controller queue, with given real parameters, when the associated device (accelerator or set of CPU cores) is available. This is done using a launching primitive (see Figure 1). The controller internals will ensure that the input data has been transferred to the device memory if needed, and that previous kernels have ended, before doing the real launch. The controller execution policy could reorder the kernels in the queue to maximize the execution efficiency as long as the input/output dependencies across kernels are not violated. The main features and contributions of our model in the kernel management are described below.
3.1.1 Kernel definition and launching: Support of both generic and specialized codes
The model provides a generic method to declare kernels. We define a kernel implementation as a tuple: (< name >,< deviceType >,< parameters_ list >,< code >), where deviceType is a symbol, or list of symbols (e.g. GPU, CPU), indicating the specific kind of device/s where the kernel can be deployed. Thus, we propose to allow the declaration of several implementations of the same kernel name, indicating the architecture/s for which this particular kernel is more suitable.
We also propose the possibility of defining kernel implementations for a generic device type. These kernels will be portable across different types of devices and architectures, and will be used by default when no specific kernel implementation is provided in the code for a given target device. As they simply consider another kernel implementation, with a different device type symbol, they can be declared in the same program together with other implementations of the same kernel name. This allows the creation of libraries of kernel implementations, developing first a generic and portable one that will work in any platform, and introducing gradually more specific and optimized kernel implementations for specific or new target devices.
The code of the generic kernels implementation, in order to be portable across different device types, should comply in our model with a set of properties (see also Section 4.6 for a discussion about an example of a generic kernel used on both multi-core CPU and GPU devices). Properties of a generic kernel: (1) The code is pure data-parallel, in the sense that it can be executed by many logical threads, and the programmer ensures that no data dependencies or race conditions can arise across them; (2) The code operates on indexed data structures using abstract 1-, 2-, or 3-dimensional thread indexes provided by the runtime system (tx , ty , tz ), that are internally adjusted for each device to access data in row-major order keeping maximum coalescing and/or vectorization properties in the specific device; (3) The code does not explicitly use resources, primitives, or synchronization mechanisms, that are specific to the programming model of a given device; (4) All accesses to data structures are done through an abstract interface provided by the runtime system, that is independent of the target device chosen (see Section 3.2.4 for our proposal to choose such an abstract interface).
A unified kernel launching function, at run-time, matches the architecture of the device to the best implementation provided at compile time for that architecture. In this way, it is possible to transparently use either generic portable kernels, optimized kernels programmed in the native models of a specific device, or even wrappers to call specialized libraries for specific device types. The controller runtime system can choose the most appropriate one.
Finally, we propose that it should be required, in the parameters list of a kernel implementation, that the programmer reports the input/output role of each kernel parameter. This will be used by the controller to detect which data should be transferred among different memory spaces, at each moment, depending on the sequence of kernels launched.
3.1.2 Characterization of kernels for execution
Our model considers the characterization of the kernel code to automatically optimize launching parameters, such as the thread-block geometries. We propose to integrate the model of qualitative characteristics presented in Ortega–Arranz et al. (2014) and Torres et al. (2013) in our controller. To use this model, the programmer should examine the kernel code, and he should conceptually characterize it in classes according to three main criteria. We introduce an extension of the kernel implementation tuple with a new element to provide the classification of the kernel code. The controller internally uses an associative table to implement the selection of launching parameters according to the rules proposed in the previously cited papers. More details about the criteria and examples are presented in Section 4.4.
3.2 Data management
Accelerators may have their own memory spaces, forcing the transfer of the input data of the kernels, and the obtained results, between the memory of the host platform and the memory of the device accelerator. The manual management of these data movements can be cumbersome and error-prone. Moreover, it is difficult to predict in advance when asynchronous data transfers are possible, or when data should stay in the accelerator device memory, as this depends on the exact sequence of kernels launching. Our model abstracts all these issues from the programmer. The main features/contributions of our model on data management are the following.
3.2.1 Data transfers between the host and the accelerators
In our model, a controller is associated, in the moment of its creation, with a particular accelerator or subset of CPU cores, and it transparently manages, in the accelerator memory space, the images of the host memory data structures. The controller can decide when and how the transfers should be carried out, depending on the data structures used in the corresponding kernels, and the sequence of kernels enqueued for launching. The programmer can use the original names of the host variables in the kernel transparently. Depending on the role of the variables (named data-structures) used as real parameters in kernel launches, we can distinguish two types: Bounded variables and internal variables.
3.2.2 Bounded variables
Bounded variables are host variables that have an image in the memory space of the accelerator (see Figure 1). The model defines an operation to bind a host variable to the controller. Once a variable is bounded, its data should not be modified by the program at the host side until an unbinding operation is applied.
The first kernel requiring the use of a bounded variable as an input will force the controller to transparently ensure that its data has already been transferred. Applying an unbinding operation to a bounded variable will force the transfer of its data from the accelerator to the host, if it has been used as output by any kernel. The main program waits until the end of the kernels using that variable, and the end of the data transmission to the host if needed.
3.2.3 Internal variables
Internal variables are variables whose scope is delimited to the kernels executed in the accelerator. They are only handled inside the memory space of the device, and they will not have allocation in the host memory space. Thus, they never imply a data transfer (see Figure 1). In the particular case of a device representing a subset of CPU cores, the memory should be transparently allocated and managed in the host device.
The model defines an operation to create an internal variable in the controller. A data structure is declared without allocating memory to it in the host. The name of this data structure is used in the creation operation to clone the type, size, and internal structure in the controller memory space. Since that moment, the name can be used as a real parameter in kernel launching as a reference of the internal variable. To destroy an internal variable, it is necessary to apply another operation using the reference name of the host variable.
3.2.4 Uniform data accesses
One of the key features that a programming model for heterogeneous systems should provide is the ability to uniformly manage the data structures on a program. As previously commented when discussing generic kernel definitions, the data accesses on the body of the kernel should be independent of the target device chosen. We propose the use of abstract thread indexes and data-accessing methods in the kernel codes. They are devised in order to design the codes to work efficiently when accessing elements in row-major order, independently of the device. We propose the use of the data-handler abstraction for arrays introduced by Hitmap (Gonzalez–Escribano et al., 2014), a library for hierarchically distributed arrays. Efficient implementations have been provided for different devices, such as CPUs, or GPUs. See more details about the Hitmap functionalities used in the implementation of our model in Section 4.1.
4 The controllers library
We have developed an implementation of the controllers model. It is designed as a library written in C99 code. Thus, it can be used to develop C/C++ programs, independently of the chosen compiler. The library defines functions, but also relies on preprocessor macros for code rewriting. Although this allows an efficient interface implementation in a compiler agnostic way, the programmer should take care to use the interface in the expected way to avoid problems derived from some common pitfalls of macro substitutions (unexpected type checking issues, misnesting due to incorrect code injection in the macro parameters, etc.).
The current implementation supports NVIDIA’s GPU devices using CUDA internally, and subsets of CPU codes using OpenMP internally. In our implementation, a kernel is a function coded for any, or for a specific computational device with particular input/output parameters. The controllers library interface defines primitives to: Create controllers; Declare and characterize kernels; Manage host data structures that can be bounded or created as internal variables in the controllers, and transparently accessed inside the kernels; and Launch the kernels.
4.1 Data structures and Hitmap
Regarding the data structures that the model handles, we have decided to integrate our implementation with Hitmap (Gonzalez–Escribano et al., 2014), an efficient library for hierarchical tiling and mapping of arrays. Hitmap defines the HitTile structure, an abstract entity for n-dimensional arrays and tiles. A HitTile structure is a handler to store array meta-data, along with the pointer to the actual memory space of the data. The circles for the variables A, B, and C in Figure 1 represent the handlers. The HitTile structure should be specialized for each array base type at the beginning of the program.
There are only four functions of Hitmap needed to work with the controllers implementation. First, hit_tileDomain and hit_tileDomainAlloc are used to declare the index domains of a tile array, also allocating the memory for the data in the second case. The function hit_tileFree is used to free the data memory and clean the handler. The function hit_tileElem is used in host or kernels code to access the elements of a tile. It receives a tile name, a number of dimensions, and the index values of the desired element. This function provides a homogeneous interface to manage data structures in both host and accelerator device kernels. The data are stored and accessed in row major order in all cases.
Hitmap library includes many other functionalities. They include management of hierarchical subselections of parts of the tiles, and transparent management of distributed-arrays, with abstract partition and communication functionalities that internally use a message-passing paradigm (exploiting MPI). This will allow the transparent integration of the controllers in distributed multi-node clusters in the future.
Most of the meta-data in the HitTile structure are only needed for advanced functionalities, such as distributed-array partitions and communications in the host code. In our new implementation, we define a new much smaller handler (HitKTile) with the minimum information needed for data accesses to multidimensional arrays through a data pointer. The kernel launching interface will transparently transform the handlers to this new type, substituting the data pointer with its equivalent in the device memory space. The data accesses inside the kernels uses this internal pointer along with a minimum number of arithmetic operations, exposing the expressions to the native compiler to open the possibility of further optimizations. The result obtains as good a performance as the classical array accesses.
In Hitmap, the implementation of different kinds of tiles hides the details of the data access for different internal data layouts to the programmer. The Hitmap library already integrates sparse domains and sparse data structures into the HitTile abstraction. The future transformation of these handler structures, to implement efficient and portable kernels, should follow a similar approach as the one used for dense arrays.
4.2 Controllers and variables management
4.2.1 Initialization and destruction
A controller is associated to a particular computational platform (accelerator or CPU-cores set) at the moment of its creation. This is done through the
The controllers associated to CPU cores internally use OpenMP. To allow the launching of CPU kernels asynchronously, as in the case of GPUs, our implementation uses OpenMP tasks. A master thread executes the host code, and one OpenMP task is activated for each core associated to a CPU controller. The controller initializes on its creation an internal array of structures with one element per assigned core task, to control their activities and their synchronization.
4.2.2 Binding variables
The function
In the current implementation, we have integrated part of the variables management in the Hitmap tile handlers. The handler stores the pointer to the device memory, a reference to the controller is bounded to, and flags to indicate the clean/dirty state of the data. This avoids the duplication of bindings, attempts to unbind variables from the wrong controller, etc. The flags help the controller to choose the proper moments to synchronize the data between the host and the device memory image.
4.2.3 Creating internal variables
The function
4.3 Declaration and configuration of kernels
A kernel is declared by using the primitive
The kernel-definition primitives declare in brackets the number of parameters of the kernel, with a tuple of information for each parameter. The parameter information includes its type, name, and input/output role:
IN: for input HitTile parameters, whose elements are only read;
OUT: for output HitTile parameters, whose elements are only written;
IO: for input and output HitTile parameters, with elements can be both read and written;
INVAL: for input parameters of any type passed by value.
For the case of accelerator devices, with separated memory spaces, this configuration allows the controllers to determine if it is necessary to carry out data transfers with the main memory of the host when kernels are launched. The primitive is followed by a structured block with the kernel code. Thus, it resembles a C function header.
The controller CPU implementation contains the loops to execute the kernel code for each element of the threads index space, that is internally assigned to a specific OpenMP thread. Both CPU and GPU kernels code use a predefined thread structure with three integer elements x,y,z indicating the 3-dimensional indexes of the corresponding fine-grain thread. In order to ease the kernel reuse across different types of device, these indexes are adapted to have the same row-major meaning in both types of kernels. Also, the space of valid indexes that can be defined in the kernel launching is independent of the threadblock sizes in GPUs. Actual threads with identifiers outside the chosen launching index space will be clipped transparently by the controller launching system. For specialized GPU kernels, the kernel code may use CUDA primitives mixed with the Hitmap data accesses that use the new portable index system.
4.4 Kernel characterization
The kernel characterization is a programmer hint to help the system to automatically determine proper kernel launching parameters in terms of special code features and platform architecture information. The CPU thread granularity in our prototype is determined by a simple regular blocking policy that does not require a specific kernel characterization.
For GPU kernels, our current prototype library integrates the model presented in Ortega–Arranz et al. (2014) and Torres et al. (2013). This model allows us to determine configuration parameters (grid, threadblock and L1 cache memory sizes), for NVIDIA’s GPUs. The primitive
Our implementation extends this characterization with the possibility to specify a fixed size for the threadblock. This is useful for kernels that rely on specific block sizes or geometries to manipulate shared memory (for example the matrix multiplication code in the CUDA Toolkit Samples).
4.5 Kernel launching
The function
the controller;
the name of the kernel;
the index space of the thread set;
the number of parameters required by the kernel;
the real parameters for the kernel execution.
The dimensions of the threads index space are specified using a CtrlThreads structure that stores up to three integer values representing the cardinality on each dimension. It is equivalent to the dim3 type in CUDA, but it is used for both, GPU or CPU kernels independently. The variable parameters should be tile variables associated to the controller, internal or bounded, or any value of the proper type for the INVAL parameters.
Internally, the execution of a GPU kernel implies: (1) the creation of the small handlers using the original tile handlers information; (2) The use of the characterization model to select the grid and threadblock geometries; and (3) the execution of a wrapper function. This wrapper reorders the thread indexes to be used as in the CPU kernels code to access data elements in row-major order, and ensures that threads outside of the required index space return immediately before executing the kernel code (see an excerpt of this code on the left of Figure 2). For CPUs, the kernel execution internally implies the partition of the index space in coarse blocks. Figure 2 (right) shows an example of a simplified code that performs this partition. The kernel launching (line 18) calls to an inline function that is generated when this kernel implementation for CPU-cores is declared. A final global synchronization is needed to preserve the launching semantic before proceeding to launch another kernel.

Excerpts of the controller library code generated for kernel deployment/launching on a CUDA capable GPU device (left), and a group of CPU-cores (right). In the CUDA version the block geometry is selected using the kernel characterization provided by the programmer. We also show the part of the wrapper function launched, that reverses the thread indexes before calling the actual kernel code. For the case of the CPU-cores, each OpenMP thread (assigned to a core) executes this code. In this example, the 2-dimensional index space is divided into blocks by rows without balancing the remaining. The code executes the loops that call the kernel code for each index-element mapped to the thread. It simulates the many-thread approach used for GPU programming using coarser-grain OpenMP tasks.
4.6 Programming example
In this section, we present a practical application of the library concepts described in the previous section by using an example. Figure 3 shows the implementation of a Stencil 2-D application. On the left of the figure, at the beginning of the program, we declare the specialized Hitmap array types to be used in the program (see line 3 in Figure 3 left).

Example of the kernel characterization and definition (left) and main code (right), for a stencil program implementing an iterative Jacobi PDE solver for the Poisson’s heat diffusion equation. The kernels are usable for both CPU and GPU controllers.

Characterization of the generic or GPU specialized kernels for the case studies (left). Example of kernel wrapper to execute a specialized GPU library function (right).
We show on the right of Figure 3 the code of the main function that coordinates the application execution.
To summarize, we observe that the final program presents an organized sequence of programming phases, that leads to a clear structure. Easy generic guidelines to program with this library can be deduced from this simple example.
5 Experimental study
This section describes the experiments we have carried out to check the functionality, and to evaluate the potential performance issues, introduced in the controller prototype implementation. We also evaluate the development effort of using the controllers prototype when compared to directly using common native programming models (CUDA or OpenMP).
5.1 Case studies
As case studies we have selected the following programs. All of them work with floating point numbers. From the CUDA Toolkit Samples, we have selected the Black–Scholes program, and two Matrix Multiplication examples, one using GPU shared-memory, and another directly calling the optimized CUBlas routine. We also use the stencil program that is shown in Figure 3. Finally, we have selected a code to independently apply a trivial recurrence equation to elements in a matrix. A quick description and the motivation to choose these examples follows. The characterizations of the kernels using the proposed model, are shown in the left of Figure 4. The parameters where chosen following the guidance presented in Ortega–Arranz et al. (2014) and Torres et al. (2013).
5.1.1 Recurrence equation
This code uses two input matrices A, B, and writes a result matrix C that is originally initialized with zeros. The kernel computes the first 500 terms of a trivial recurrence equation that involves only two single addition operations: Cx (i,j) = C x − 1(i,j) + A(i,j) + B(i,j), providing the last term computed for each position as the result. There is only one kernel launching for the whole program. It is an embarrassing parallel code, where each element is computed independently. The code involves a high number of independent and repetitive coalesced reads and writes to global memory on each thread. This artificial code is specifically designed to test the efficiency of the data accesses through the new small tile handlers inside the kernel codes. A common kernel is used for both CPU and GPU controllers.
5.1.2 Black–Scholes
The Black–Scholes formula is based on a mathematical model of a financial market. The result estimates the price of European-style options. The program in the CUDA Toolkit Sample independently applies the formula to a chosen number of input values stores in an array, calculating and storing their results. Thus, it is also an embarrassing parallel program with perfectly coalesced accesses on a GPU. Each thread does only one read and one write operation to global memory. It applies several floating point operations calculating intermediate result stored in registers or temporal variables. The kernel is called 512 times consecutively. This program is adequate to measure the cost of multiple kernel calls of more sophisticated arithmetic computation, with a very low number of global memory accesses per thread. A common kernel is used for both CPU and GPU controllers.
5.1.3 Stencil computation
This program computes the stability point of a Partial Differential Equation (PDE), in this case the Poisson’s equation the heat diffusion. It uses a Jacobi iterative method on a 2-dimensional discretized space, represented as a matrix. The program implementation is shown in Figure 3. It is an 4-point stencil program that executes a fixed number of time iterations. On each iteration it independently computes a new value for each cell in the matrix, using the information of the four neighbour cells.
This kernel presents a similar number of arithmetic operations per thread as global memory accesses. The read operations are not completely coalesced due to the neighbour accesses, and the positions read by each thread are overlapped with the neighbour threads. A neighbour synchronization is needed at the end of each iteration, before a new simulation step starts. This kernel measures the effects of non-completely coalesced global memory accesses.
These stencil simulation programs are usually optimized to swap the two data structures (the one read, and the one write) after each iteration. We have intentionally skipped this optimization, implementing another kernel that simply copies the new generated data from the written variable to the original one. It shows the ability of the characterization model to provide different launching parameters for different kernels in the same program. The program is based on a repetitive invocation of two kernels with very low load per thread. Thus, it tests the efficiency of the implementation of the kernel launching procedures. Again, the same kernel is defined for both CPU and GPU platforms.
5.1.4 Matrix multiplication
The Matrix multiplication computes the product of two different square matrices, storing the result in a third one: C = A * B. The computation of each cell of the resulting matrix is not dependent on another computation. Nevertheless, different cells used elements of A or B that are also read by other cell computations. Thus, data can be reused and shared across the computation of each cell. Moreover the read patterns on A and B matrices should be studied to exploit coalescence in GPUs, and properly exploit caches in CPUs. These lead to interesting optimizations in both GPU and CPU devices. Thus, we use different specialized kernels for each kind of devices.
A direct simple solution to this problem involves one generic kernel, using a bidimensional grid of threads, for both CPU and GPU. Each thread ti,j
is responsible of computing the dot product operation
For the GPU controllers we have a second implementation. It shows the functionality of our kernel-wrapper facility, that allows one to implement and launch as a kernel a call to the optimized cuBLAS routine for matrix multiplication (see the right of Figure 4). The wrapper kernels do not need characterization, as they contain only host code, and the library routines include the code that make the decisions about launching parameters.
5.2 Development effort and code complexity
The first part of our experimental study evaluates how the use of our proposed model affects the development effort when compared with using the native programming models for the two types of devices considered in the current version of the prototype: CUDA for GPUs, and OpenMP for multi-core CPUs.
We measure three classical development effort and code complexity metrics: COCOMO lines of code, number of tokens, and McCabe’s cyclomatic complexity. The first two ones measure the volume of code that the programmer should develop. The third one measures the rational effort needed to program it in terms of code divergences and potential casuistry that should be considered to develop, test, and debug. The metrics are applied to the part of the code that includes the kernel, the functions invoked by them, and the host coordination code. We ignore input data initialization, error or results checking, performance instrumentation, and writing messages to the standard output. Thus, the considered host code includes the declaration, creation and initialization of controller, data containers, and structures for parallelism coordination, the memory transfer between host and devices, and the kernel launching operations.
Table 1 (left) shows the measurements for these metrics in the baseline versions, using OpenMP for the CPU variant, and CUDA for the GPU variant, and the versions using our controllers library interface. The results indicate that programming with controllers for GPUs generates a significantly lower volume of code, and a reduced cyclomatic complexity, indicating a clearly lower development effort than using CUDA. A closer look at the codes indicates that most of the reduction is found in the host part of the codes, as expected. On the other hand, for this kind of data parallel codes, the comparisons with OpenMP codes show that using the controllers interface reduces the cyclomatic complexity slightly, but increases the code volume.
Measurements of the development effort metrics for the codes of the case studies (left). Comparison in terms of the percentage of words that are common and can be reused, should be deleted, or should be changed, when porting codes between GPU and CPU versions using the native models, or the controllers model (right).
The main advantage of considering the CPU cores as an accelerator device in the controller model is found in the portability of code between GPUs and CPUs. Table 1 (right) shows the percentage of words that can be reused, should be deleted, or should be changed to port from a CUDA program to the equivalent OpenMP version; and the same measurements when porting from a controller version for GPUs to the equivalent controller version for CPUs. The results clearly indicate that the portability of the controller versions across different device types is really high in the first three cases, and still significant for the matrix multiplication program, that includes different specialized kernels. In this case the coordination code using controllers is still the same, deriving in a significantly lesser number of words to change than when porting between CUDA and OpenMP.
5.3 Performance study
The second part of our experimental study measures the impact of using our controllers prototype in terms of performance. We have run the GPU implementations in an NVIDIA’s GeForce Titan Black X, with CUDA capability 5.2, installed on a host machine named Hydra, with two CPUs Intel Xeon E5-2609 v3 @1.90GHz, and 64Gb DDR3 main memory. To test the CPU implementation we have used a shared-memory machine with a higher number of cores. It is named Heracles, and it is a Dell PowerEdge R815 server, with 2 AMD Opteron 6376 processors at 2.3 GHz, with 16 cores per processor, and a total of 32 cores. The operating system in both machines is a Linux Centos 7 OS. The programs have been compiled using the CUDA Toolkit 7.5, and GCC 4.8.3. We have used the flags -arch=compute_52, -O3, and the -mtune choice appropriate for each host machine.
We present comparisons of the execution times skipping input data initialization, results checking, and control messages writing. The instrumented code includes controllers creation and variables binding/unbinding operations (which may imply data transfers). In the same way, CUDA data copies between the host and the device memories are also included. In the case of GPU programs, we also measure the execution time accumulated by the kernels launching alone, without memory transfer operations. Due to instabilities in the execution times of the host codes and data transfers, the results always show the mean of the execution time obtained on five repetitions of each test.
For the CPU versions we have selected input sizes with enough load to achieve scalability in our test machine with up to 32 threads. The exact input size parameters chosen are presented in Table 2. The total execution times of the baseline programs and the versions based on controllers are presented in Figure 5, for different number of active threads/cores. The results indicate that our implementation of the controller abstraction for CPUs does not implies significant overheads for the scalability ranges tested.
Input data sizes and number of iterations selected for each case study in the performance experimental study.

Execution times (seconds) in Heracles machine of the baseline (Base) and the controller versions for CPU device (Ctrl.CPU) with a variable number of cores.
For a fair comparison, both GPU approaches (CUDA and controller based) use the same values for the configuration of launching parameters. In the case of the Black–Scholes program, the threadblock size value predicted by the characterization model is the same as that found in the original CUDA code. In the matrix multiplication case, it is fixed in both cases to 32 × 32. For the other two case studies, the values predicted by the characterization model are injected manually in the baseline versions. These values have shown to produce at least 96% of the best performance obtained with other threadblock geometries. We compare the performance obtained when launching the baseline and the controllers versions with different input data sizes, selected to produce from very low to significant execution times (from tenths of a second, to more than four seconds). The exact input size parameters are presented in Table 2, with a class name (S, M, L, XL) for easier reference in the following discussion.
The results for the GPU programs are presented in Table 3. We skip to the present times for the matrix multiplication using the cuBLAS library. The memory transfers and launching operations are enclosed into the library routine, that is called by both the CUDA and the wrapper virtual kernel in the controller version. Thus, the performance is the same except for stochastic behaviours that affect both codes equally.
Execution time (seconds) for the case studies versions using CUDA, or controllers for GPUs, with different input sizes. Host time measures the data communications, coordination code, and launching operations. Kernels time measures the actual kernel execution times. Total times are the addition of both times for easy comparison.
The execution times of the kernels show a little overhead when using the controller interface, due to an extra internal stage for thread index conversion and threads space clipping. Nevertheless, the overhead is always less than 1% of the kernels mean execution time. The recurrence equation kernel, specifically designed to test the efficiency of the data accesses through the small kernel tile handlers, shows the minimum overhead of the four cases.
As expected, the execution times of the host codes, in both CUDA and controller versions, are smaller than the corresponding kernels execution, specially for large input sizes (L and XL), that present significant kernel execution times. These times spent on host code coordination, and memory transfers, are less stable than the kernels execution times. Sometimes, the controller versions show even better results than the equivalent host code in the CUDA versions. The same memory transfers in the CUDA codes are internally executed in the controller versions. Memory transfers have the less predictable times, with a higher variance. The rest of the controller operations are in general light loaded. The critical one is the kernel launching, that involves several checks and handler manipulations. Nevertheless, the overhead of the launching operation has been measured to be less than 1×10−5 seconds for the GPU implementation, in the machine used for this study. Thus, the accumulated overhead of many launching operations in the Black–Scholes, or the Jacobi programs, is not significant even for the smaller input sizes tested (class S). This overhead could only be clearly noticeable when executing long sequences of kernels with really low computational load, which are not in general appropriate for parallelism exploitation.
6 Conclusions
In this paper we propose the controller model, a parallel programming model that simplifies the coding of applications for heterogeneous systems. It is based on an abstract entity, the controller, that manages the launching of kernel sequences on accelerators, or sets of CPU cores.
It provides mechanisms to: (1) Associate controllers to devices; (2) Define portable kernels that can be reused across different types of devices; (3) Define specialized kernels for the same program on different device types; (4) Automatically select proper values for launching parameters on different devices through a characterization of the kernels provided by the programmer, and; (5) Automatically deal with different memory spaces of the host and the devices when needed. This model unifies the kernel programming and data structures management, bringing together the accelerator and multi-threaded programming, taking into account the architectural differences of the accelerator platforms to obtain good performance. Our experimental study shows the advantages of using this approach in terms of development effort metrics, and the efficiency of our prototype implementation for several case studies representing different computational costs, and global memory accesses scenarios.
Our future work includes the extension of the prototype in order to support the management of other types of accelerators, such as XeonPhi coprocessors; further CPU kernel optimizations; and other techniques that exploit the modern features of modern accelerators, such as asynchronous communications, communication-computation overlapping, and different kernel launching policies. The use of Hitmap partition and communication functionalities will be explored to use the controllers in higher scale distributed multi-node environments.
Footnotes
Declaration of Conflicting Interests
The author(s) declared no potential conflicts of interest with respect to the research, authorship, and/or publication of this article.
Funding
The author(s) disclosed receipt of the following financial support for the research, authorship, and/or publication of this article: This work was partially supported by MICINN (Spain) and ERDF program of the European Union: HomProg-HetSys project (TIN2014-58876-P), and COST Program Action IC1305: Network for Sustainable Ultrascale Computing (NESUS).
