| # Offloading Design & Internals |
| |
| ```{contents} |
| :local: true |
| ``` |
| |
| ## Introduction |
| |
| This document describes the Clang driver and code generation steps for creating |
| offloading applications. Clang supports offloading to various architectures |
| using programming models like CUDA, HIP, and OpenMP. The purpose of this |
| document is to illustrate the steps necessary to create an offloading |
| application using Clang. |
| |
| ## OpenMP Offloading |
| |
| Clang supports OpenMP target offloading to several different architectures such |
| as NVPTX, AMDGPU, X86_64, Arm, and PowerPC. Offloading code is generated by |
| Clang and then executed using the `libomptarget` runtime and the associated |
| plugin for the target architecture, e.g. `libomptarget.rtl.cuda`. This section |
| describes the steps necessary to create a functioning device image that can be |
| loaded by the OpenMP runtime. More information on the OpenMP runtimes can be |
| found at the [OpenMP documentation page](https://openmp.llvm.org). |
| |
| (offloading-overview)= |
| |
| ### Offloading Overview |
| |
| The goal of offloading compilation is to create an executable device image that |
| can be run on the target device. OpenMP offloading creates executable images by |
| compiling the input file for both the host and the target device. The output |
| from the device phase then needs to be embedded into the host to create a fat |
| object. A special tool then needs to extract the device code from the fat |
| objects, run the device linking step, and embed the final image in a symbol the |
| host runtime library can use to register the library and access the symbols on |
| the device. |
| |
| #### Compilation Process |
| |
| The compiler performs the following high-level actions to generate OpenMP |
| offloading code: |
| |
| - Compile the input file for the host to produce a bitcode file. Lower `#pragma |
| omp target` declarations to [offloading entries](#generating-offloading-entries) |
| and create metadata to indicate which entries are on the device. |
| |
| - Compile the input file for the target [device](#device-compilation) using |
| the [offloading entry](#generating-offloading-entries) metadata created |
| by the host. |
| |
| - Link the OpenMP device runtime library and run the backend to create a device |
| object file. |
| |
| - Run the backend on the host bitcode file and create a [fat object file](#creating-fat-objects) |
| using the device object file. |
| |
| - Pass the fat object file to the [linker wrapper tool](#linking-target-device-code) |
| and extract the device objects. Run the device linking action on the extracted |
| objects. |
| |
| - [Wrap](#device-binary-wrapping) the [device images](#linking-target-device-code) |
| and [offload entries](#generating-offloading-entries) in a symbol that |
| can be accessed by the host. |
| |
| - Add the [wrapped binary](#device-binary-wrapping) to the linker input and |
| run the host linking action. Link with `libomptarget` to register and |
| execute the images. |
| |
| (generating-offloading-entries)= |
| |
| ### Generating Offloading Entries |
| |
| The first step in compilation is to generate offloading entries for the host. |
| This information is used to identify function kernels or global values that will |
| be provided by the device. Blocks contained in a `#pragma omp target` or |
| symbols inside a `#pragma omp declare target` directive will have offloading |
| entries generated. The following table shows the {ref}`offload entry structure |
| <table-tgt_offload_entry_structure>`. |
| |
| :::{list-table} __tgt_offload_entry Structure |
| :name: table-tgt_offload_entry_structure |
| :header-rows: 1 |
| |
| * - Type |
| - Identifier |
| - Description |
| * - `void*` |
| - `addr` |
| - Address of global symbol within device image (function or global) |
| * - `char*` |
| - `name` |
| - Name of the symbol |
| * - `size_t` |
| - `size` |
| - Size of the entry info (0 if it is a function) |
| * - `int32_t` |
| - `flags` |
| - Flags associated with the entry (see {ref}`table-offload_entry_flags`) |
| * - `int32_t` |
| - `reserved` |
| - Reserved, to be used by the runtime library. |
| ::: |
| |
| The address of the global symbol will be set to the device pointer value by the |
| runtime once the device image is loaded. The flags are set to indicate the |
| handling required for the offloading entry. If the offloading entry is an entry |
| to a target region it can have one of the following {ref}`entry flags |
| <table-offload_entry_flags>`. |
| |
| :::{table} Target Region Entry Flags |
| :name: table-offload_entry_flags |
| |
| | Name | Value | Description | |
| | ---------------------------------- | ------ | --------------------------------------- | |
| | `OMPTargetRegionEntryTargetRegion` | `0x00` | Mark the entry as generic target region | |
| | `OMPTargetRegionEntryCtor` | `0x02` | Mark the entry as a global constructor | |
| | `OMPTargetRegionEntryDtor` | `0x04` | Mark the entry as a global destructor | |
| ::: |
| |
| If the offloading entry is a global variable, indicated by a non-zero size, it |
| will instead have one of the following {ref}`global |
| <table-offload_global_flags>` flags. |
| |
| :::{list-table} Target Region Global |
| :name: table-offload_global_flags |
| :header-rows: 1 |
| |
| * - Name |
| - Value |
| - Description |
| * - `OMPTargetGlobalVarEntryTo` |
| - `0x00` |
| - Mark the entry as a 'to' attribute (w.r.t. the to clause) |
| * - `OMPTargetGlobalVarEntryLink` |
| - `0x01` |
| - Mark the entry as a 'link' attribute (w.r.t. the link clause) |
| ::: |
| |
| The target offload entries are used by the runtime to access the device kernels |
| and globals that will be provided by the final device image. Each offloading |
| entry is set to use the `omp_offloading_entries` section. When the final |
| application is created the linker will provide the |
| `__start_omp_offloading_entries` and `__stop_omp_offloading_entries` symbols |
| which are used to create the [final image](#device-binary-wrapping). |
| |
| This information is used by the device compilation stage to determine which |
| symbols need to be exported from the device. We use the `omp_offload.info` |
| metadata node to pass this information device compilation stage. |
| |
| #### Accessing Entries on the Device |
| |
| Accessing the entries in the device is done using the address field in the |
| {ref}`offload entry<table-tgt_offload_entry_structure>`. The runtime will set |
| the address to the pointer associated with the device image during runtime |
| initialization. This is used to call the corresponding kernel function when |
| entering a `#pragma omp target` region. For variables, the runtime maintains a |
| table mapping host pointers to device pointers. Global variables inside a |
| `#pragma omp target declare` directive are first initialized to the host's |
| address. Once the device address is initialized we insert it into the table to |
| map the host address to the device address. |
| |
| #### Debugging Information |
| |
| We generate structures to hold debugging information that is passed to |
| `libomptarget`. This allows the front-end to generate information the runtime |
| library uses for more informative error messages. This is done using the |
| standard {ref}`identifier structure <table-ident_t_structure>` used in |
| `libomp` and `libomptarget`. This is used to pass information and source |
| locations to the runtime. |
| |
| :::{list-table} ident_t Structure |
| :name: table-ident_t_structure |
| :header-rows: 1 |
| |
| * - Type |
| - Identifier |
| - Description |
| * - `int32_t` |
| - `reserved` |
| - Reserved, to be used by the runtime library. |
| * - `int32_t` |
| - `flags` |
| - Flags used to indicate some features, mostly unused. |
| * - `int32_t` |
| - `reserved` |
| - Reserved, to be used by the runtime library. |
| * - `int32_t` |
| - `reserved` |
| - Reserved, to be used by the runtime library. |
| * - `char*` |
| - `psource` |
| - Program source information, stored as `;filename;function;line;column;;\0` |
| ::: |
| |
| If debugging information is enabled, we will also create strings to indicate the |
| names and declarations of variables mapped in target regions. These have the |
| same format as the source location in the {ref}`identifier structure |
| <table-ident_t_structure>`, but the function name is replaced with the variable |
| name. |
| |
| (device-compilation)= |
| |
| ### Offload Device Compilation |
| |
| The input file is compiled for each active device toolchain. The device |
| compilation stage is performed differently from the host stage. Namely, we do |
| not generate any offloading entries. This is set by passing the |
| `-fopenmp-is-target-device` flag to the front-end. We use the host bitcode to |
| determine which symbols to export from the device. The bitcode file is passed in |
| from the previous stage using the `-fopenmp-host-ir-file-path` flag. |
| Compilation is otherwise performed as it would be for any other target triple. |
| |
| When compiling for the OpenMP device, we set the visibility of all device |
| symbols to be `protected` by default. This improves performance and prevents a |
| class of errors where a symbol in the target device could preempt a host |
| library. |
| |
| The OpenMP runtime library is linked in during compilation to provide the |
| implementations for standard OpenMP functionality. For GPU targets this is done |
| by linking in a special bitcode library during compilation, (e.g. |
| `libomptarget-nvptx64-sm_70.bc`) using the `-mlink-builtin-bitcode` flag. |
| Other device libraries, such as CUDA's libdevice, are also linked this way. If |
| the target is a standard architecture with an existing `libomp` |
| implementation, that will be linked instead. Finally, device tools are used to |
| create a relocatable device object file that can be embedded in the host. |
| |
| (creating-fat-objects)= |
| |
| ### Creating Fat Objects |
| |
| A fat binary is a binary file that contains information intended for another |
| device. We create a fat object by embedding the output of the device compilation |
| stage into the host as a named section. The output from the device compilation |
| is passed to the host backend using the `-fembed-offload-object` flag. This |
| embeds the device image into the `.llvm.offloading` section using a special |
| binary format that behaves like a string map. This binary format is used to |
| bundle metadata about the image so the linker can associate the proper device |
| linking action with the image. Each device image will start with the magic bytes |
| `0x10FF10AD`. |
| |
| ```llvm |
| @llvm.embedded.object = private constant [1 x i8] c"\00", section ".llvm.offloading" |
| ``` |
| |
| The device code will then be placed in the corresponding section one the backend |
| is run on the host, creating a fat object. Using fat objects allows us to treat |
| offloading objects as standard host objects. The final object file should |
| contain the following {ref}`offloading sections <table-offloading_sections>`. We |
| will use this information when [linking target device code](#linking-target-device-code). |
| |
| :::{list-table} Offloading Sections |
| :name: table-offloading_sections |
| :header-rows: 1 |
| |
| * - Section |
| - Description |
| * - `omp_offloading_entries` |
| - Offloading entry information (see {ref}`table-tgt_offload_entry_structure`) |
| * - `.llvm.offloading` |
| - Embedded device object file for the target device and architecture |
| ::: |
| |
| (device-linking)= |
| |
| ### Linking Target Device Code |
| |
| Objects containing {ref}`table-offloading_sections` require special handling to |
| create an executable device image. This is done using a Clang tool, see |
| {doc}`ClangLinkerWrapper` for more information. This tool works as a wrapper |
| over the host linking job. It scans the input object files for the offloading |
| section `.llvm.offloading`. The device files stored in this section are then |
| extracted and passed to the appropriate linking job. The linked device image is |
| then [wrapped](#device-binary-wrapping) to create the symbols used to load |
| the device image and link it with the host. |
| |
| The linker wrapper tool supports linking bitcode files through link time |
| optimization (LTO). This is used whenever the object files embedded in the host |
| contain LLVM bitcode. Bitcode will be embedded for architectures that do not |
| support a relocatable object format, such as AMDGPU or SPIR-V, or if the user |
| requested it using the `-foffload-lto` flag. |
| |
| (device-binary-wrapping)= |
| |
| ### Device Binary Wrapping |
| |
| Various structures and functions are used to create the information necessary to |
| offload code on the device. We use the [linked device executable](#linking-target-device-code) |
| with the corresponding offloading entries to create the symbols |
| necessary to load and execute the device image. |
| |
| #### Structure Types |
| |
| Several different structures are used to store offloading information. The |
| {ref}`device image structure <table-device_image_structure>` stores a single |
| linked device image and its associated offloading entries. The offloading |
| entries are stored using the `__start_omp_offloading_entries` and |
| `__stop_omp_offloading_entries` symbols generated by the linker using the |
| {ref}`table-tgt_offload_entry_structure`. |
| |
| :::{table} __tgt_device_image Structure |
| :name: table-device_image_structure |
| |
| | Type | Identifier | Description | |
| | ----------------------- | -------------- | -------------------------------------- | |
| | `void*` | `ImageStart` | Pointer to the target code start | |
| | `void*` | `ImageEnd` | Pointer to the target code end | |
| | `__tgt_offload_entry*` | `EntriesBegin` | Begin of table with all target entries | |
| | `__tgt_offload_entry*` | `EntriesEnd` | End of table (non inclusive) | |
| ::: |
| |
| The target {ref}`target binary descriptor <table-target_binary_descriptor>` is |
| used to store all binary images and offloading entries in an array. |
| |
| :::{table} __tgt_bin_desc Structure |
| :name: table-target_binary_descriptor |
| |
| | Type | Identifier | Description | |
| | ----------------------- | ------------------ | ------------------------------------------ | |
| | `int32_t` | `NumDeviceImages` | Number of device types supported | |
| | `__tgt_device_image*` | `DeviceImages` | Array of device images (1 per dev. type) | |
| | `__tgt_offload_entry*` | `HostEntriesBegin` | Begin of table with all host entries | |
| | `__tgt_offload_entry*` | `HostEntriesEnd` | End of table (non inclusive) | |
| ::: |
| |
| ### Global Variables |
| |
| {ref}`table-global_variables` lists various global variables, along with their |
| type and their explicit ELF sections, which are used to store device images and |
| related symbols. |
| |
| :::{list-table} Global Variables |
| :name: table-global_variables |
| :header-rows: 1 |
| |
| * - Variable |
| - Type |
| - ELF Section |
| - Description |
| * - `__start_omp_offloading_entries` |
| - `__tgt_offload_entry` |
| - `.omp_offloading_entries` |
| - Begin symbol for the offload entries table. |
| * - `__stop_omp_offloading_entries` |
| - `__tgt_offload_entry` |
| - `.omp_offloading_entries` |
| - End symbol for the offload entries table. |
| * - `__dummy.omp_offloading.entry` |
| - `__tgt_offload_entry` |
| - `.omp_offloading_entries` |
| - Dummy zero-sized object in the offload entries section to force linker to |
| define begin/end symbols defined above. |
| * - `.omp_offloading.device_image` |
| - `__tgt_device_image` |
| - `.omp_offloading_entries` |
| - ELF device code object of the first image. |
| * - `.omp_offloading.device_image.N` |
| - `__tgt_device_image` |
| - `.omp_offloading_entries` |
| - ELF device code object of the (N+1)th image. |
| * - `.omp_offloading.device_images` |
| - `__tgt_device_image` |
| - `.omp_offloading_entries` |
| - Array of images. |
| * - `.omp_offloading.descriptor` |
| - `__tgt_bin_desc` |
| - `.omp_offloading_entries` |
| - Binary descriptor object (see {ref}`binary-descriptor`) |
| ::: |
| |
| (binary-descriptor)= |
| |
| #### Binary Descriptor for Device Images |
| |
| This object is passed to the offloading runtime at program startup and it |
| describes all device images available in the executable or shared library. It |
| is defined as follows: |
| |
| ```c |
| __attribute__((visibility("hidden"))) |
| extern __tgt_offload_entry *__start_omp_offloading_entries; |
| __attribute__((visibility("hidden"))) |
| extern __tgt_offload_entry *__stop_omp_offloading_entries; |
| static const char Image0[] = { <Bufs.front() contents> }; |
| ... |
| static const char ImageN[] = { <Bufs.back() contents> }; |
| static const __tgt_device_image Images[] = { |
| { |
| Image0, /*ImageStart*/ |
| Image0 + sizeof(Image0), /*ImageEnd*/ |
| __start_omp_offloading_entries, /*EntriesBegin*/ |
| __stop_omp_offloading_entries /*EntriesEnd*/ |
| }, |
| ... |
| { |
| ImageN, /*ImageStart*/ |
| ImageN + sizeof(ImageN), /*ImageEnd*/ |
| __start_omp_offloading_entries, /*EntriesBegin*/ |
| __stop_omp_offloading_entries /*EntriesEnd*/ |
| } |
| }; |
| static const __tgt_bin_desc BinDesc = { |
| sizeof(Images) / sizeof(Images[0]), /*NumDeviceImages*/ |
| Images, /*DeviceImages*/ |
| __start_omp_offloading_entries, /*HostEntriesBegin*/ |
| __stop_omp_offloading_entries /*HostEntriesEnd*/ |
| }; |
| ``` |
| |
| #### Global Constructor and Destructor |
| |
| The global constructor (`.omp_offloading.descriptor_reg()`) registers the |
| device images with the runtime by calling the `__tgt_register_lib()` runtime |
| function. The constructor is explicitly defined in `.text.startup` section and |
| is run once when the program starts. Similarly, the global destructor |
| (`.omp_offloading.descriptor_unreg()`) calls `__tgt_unregister_lib()` for |
| the destructor and is also defined in `.text.startup` section and run when the |
| program exits. |
| |
| ### Offloading Example |
| |
| This section contains a simple example of generating offloading code using |
| OpenMP offloading. We will use a simple `ZAXPY` BLAS routine. |
| |
| ```c++ |
| #include <complex> |
| |
| using complex = std::complex<double>; |
| |
| void zaxpy(complex *X, complex *Y, complex D, std::size_t N) { |
| #pragma omp target teams distribute parallel for |
| for (std::size_t i = 0; i < N; ++i) |
| Y[i] = D * X[i] + Y[i]; |
| } |
| |
| int main() { |
| const std::size_t N = 1024; |
| complex X[N], Y[N], D; |
| #pragma omp target data map(to:X[0 : N]) map(tofrom:Y[0 : N]) |
| zaxpy(X, Y, D, N); |
| } |
| ``` |
| |
| This code is compiled using the following Clang flags. |
| |
| ```console |
| $ clang++ -fopenmp -fopenmp-targets=nvptx64 -O3 zaxpy.cpp -c |
| ``` |
| |
| The output section in the object file can be seen using the `readelf` utility. |
| The `.llvm.offloading` section has the `SHF_EXCLUDE` flag so it will be |
| removed from the final executable or shared library by the linker. |
| |
| ```text |
| $ llvm-readelf -WS zaxpy.o |
| Section Headers: |
| [Nr] Name Type Address Off Size ES Flg Lk Inf Al |
| [11] omp_offloading_entries PROGBITS 0000000000000000 0001f0 000040 00 A 0 0 1 |
| [12] .llvm.offloading PROGBITS 0000000000000000 000260 030950 00 E 0 0 8 |
| ``` |
| |
| Compiling this file again will invoke the `clang-linker-wrapper` utility to |
| extract and link the device code stored at the section named |
| `.llvm.offloading` and then use entries stored in |
| the section named `omp_offloading_entries` to create the symbols necessary for |
| `libomptarget` to register the device image and call the entry function. |
| |
| ```console |
| $ clang++ -fopenmp -fopenmp-targets=nvptx64 zaxpy.o -o zaxpy |
| $ ./zaxpy |
| ``` |
| |
| We can see the steps created by clang to generate the offloading code using the |
| `-ccc-print-phases` option in Clang. This matches the description in |
| [Offloading Overview](#offloading-overview). |
| |
| ```console |
| $ clang++ -fopenmp -fopenmp-targets=nvptx64 -ccc-print-phases zaxpy.cpp |
| # "x86_64-unknown-linux-gnu" - "clang", inputs: ["zaxpy.cpp"], output: "/tmp/zaxpy-host.bc" |
| # "nvptx64-nvidia-cuda" - "clang", inputs: ["zaxpy.cpp", "/tmp/zaxpy-e6a41b.bc"], output: "/tmp/zaxpy-07f434.s" |
| # "nvptx64-nvidia-cuda" - "NVPTX::Assembler", inputs: ["/tmp/zaxpy-07f434.s"], output: "/tmp/zaxpy-0af7b7.o" |
| # "x86_64-unknown-linux-gnu" - "clang", inputs: ["/tmp/zaxpy-e6a41b.bc", "/tmp/zaxpy-0af7b7.o"], output: "/tmp/zaxpy-416cad.o" |
| # "x86_64-unknown-linux-gnu" - "Offload::Linker", inputs: ["/tmp/zaxpy-416cad.o"], output: "a.out" |
| ``` |
| |
| ### Relocatable Linking |
| |
| The offloading compilation pipeline normally will defer the final device linking |
| and runtime registration until the `clang-linker-wrapper` is run to create the |
| executable. This is the standard behaviour when compiling for OpenMP offloading |
| or CUDA and HIP in `-fgpu-rdc` mode. However, there are some cases where the |
| user may wish to perform this device handling prematurely. This is described in |
| the {doc}`linker wrapper documentation<ClangLinkerWrapper>`. |
| |
| Effectively, this allows the user to handle offloading specific linking ahead of |
| time when shipping objects or static libraries. This can be thought of as |
| performing a standard `-fno-gpu-rdc` compilation on a subset of object files. |
| This can be useful to reduce link time, prevent users from interacting with the |
| library's device code, or for shipping libraries to incompatible compilers. |
| |
| Normally, if a relocatable link is done using `clang -r` it will simply merge |
| the `.llvm.offloading` sections which will then be linked later when the |
| executable is created. However, if the `-r` flag is used with the offloading |
| toolchain, it will perform the device linking and registration phases and then |
| merge the registration code into the final relocatable object file. |
| |
| The following example shows how using the relocatable link with the offloading |
| pipeline can create a static library with offloading code that can be |
| redistributed without requiring any additional handling. |
| |
| ```console |
| $ clang++ -fopenmp -fopenmp-targets=nvptx64 foo.cpp -c |
| $ clang++ -lomptarget.devicertl --offload-link -r foo.o -o merged.o |
| $ llvm-ar rcs libfoo.a merged.o |
| # g++ app.cpp -L. -lfoo |
| ``` |