A few days ago I posted a pitch for our Integrated Distributed ThinLTO project
And now I’m posing a compete RFC.
Integrated Distributed ThinLTO
Goal
We have customers with LLVM-based toolchains that use a variety of different build and distribution systems including but not limited to our own. We’d like to implement support for an integrated DistributedThinLTO (DTLTO) approach to improve their build performance, while making adoption of DTLTO seamless, as easy as adding a couple of options on the command line.
1. Challenges
DTLTO is more complex for integration into existing build systems, because build rule dependencies are not known in advance. They become available only after DTLTO’s ThinLink phase completes and for each of the bitcode files a list of its import files becomes known.
2. Dynamic dependency problem solution
Let’s take an example and assume that we have a Makefile rule for performing the regular ThinLTO step that looks like this:
program.elf: main.o file1.o libsupport.a
$(LD) --lto=thin main.o file1.o -lsupport -o program.elf
Let’s use the following example:
- main.o, file1.o are bitcode files
- libsupport.a is an archive containing two bitcode files: file2.o and file3.o
- main.o imports from file1.o and file2.o (part of libsupport.a)
- file1.o imports from file2.o
- file2.o has no dependencies (it doesn’t import from my other bitcode file)
- file3.o (part of libsupport.a) is not referenced
For Distributed ThinLTO, we will need to find a way to overcome the problem of the dynamic dependencies described in section 1.
In the case of a build system based on Makefiles (such as Icecream or DistCC), we need the linker to produce an additional makefile that will contain the build rule with a dynamically calculated dependencies list.
So, in the first rule, the linker, when given a special option -dtlto, will generate an additional makefile.
distr.makefile: main.o file1.o libsupport.a
$(LD) main.o file1.o --dtlto distr.makefile -lsupport -o program.elf
It will also implicitly generate individual module summary index files <filename>.thintlo.bc corresponding to each of the input bitcode files.
Let’s use the following conventions:
- .o – bitcode file
- .native.o – native object file
- .thintlo.bc – individual summary index file
- –dtlto <makefile_name> – option for producing an additional makefile for the second makefile rule; it also implicitly produces the set of individual module summary index files
Here is the body of distr.makefile file that the linker generates in the first rule described above:
DIST_CC := <path to a tool that can distribute ThinLTO codegen job>
main.native.o : main.thinlto.bc main.o file1.o file2.o
$(DIST_CC) clang –thinlto-index=main.thinlto.bc main.o file1.o file2.o -o main.native.o
file1.native.o : file1.thinlto.bc file1.o file2.o
$(DIST_CC) clang –thinlto-index=file1.thinlto.bc file1.o file2.o -o file1.native.o
file2.native.o : file2.thinlto.bc file2.o
$(DIST_CC) clang –thinlto-index=file2.thinlto.bc file2.o -o file2.native.o
program.elf: main.native.o file1.native.o file2.native.o
$(LD) main.native.o file1.native.o file2.native.o -o program.elf
In the second rule, the linker itself could invoke $(MAKE) with this additional makefile distr.makefile to produce target executable:
program.elf: distr.makefile
$(MAKE) -j<N> -f distr.makefile
This option –dtlto= <makefile_name> was introduced for this RFC only to simplify the explanations of semantics for these rules. Since the linker performs both rules, there is no need to pass an option to choose the name of the makefile, the linker could take care of it.
From the user’s perspective, the original make rule will require only two small modifications, namely adding one additional option on the command line to tell the linker to do the distribution (–thinlto-distribute) and which distribution system to use (–thinlto-distribution-tool=“path to a tool that distributes ThinLTO codegen job”). These options needs to get implemented in the linker.
So, if the original rule for ThinLTO looked like this:
program.elf: main.o file1.o libsupport.a
$(LD) --lto=thin main.o file1.o -lsupport -o program.elf
in order to enable DTLTO, the user simply needs to change the rule like this:
program.elf : main.o file1.o libsupport.a
$(LD) --lto=thin –thinlto-distribute –thinlto-distribution-tool==$(DIST_CC) main.o file1.o -lsupport -o program.elf
Note that no additional work needs to be done by the user or the build system to handle archives. All this work is done by the linker.
3. Overview of existing popular Open Source & Proprietary systems that could be used for ThinLTO codegen distribution
Some or all of these systems could be potentially supported, bringing a lot of value for the ThinLTO customers who have already deployed one of these systems.
- Distcc
- Icecream
- FastBuild
- Incredibuild; Incredibuild is one of the most popular proprietary build systems.
- SN-DBS; SN-DBS is a proprietary distributed build system developed by SN Systems, which is part of Sony. SN-DBS uses job description documents in the form of JSON files for distributing jobs across the network. In Sony, we already have an internal production level DTLTO implementation using SN-DBS. In our implementation, the linker is responsible for generating the JSON build files.
4. Challenges & problems
This section describes the challenges that we encountered when implementing DTLTO integration with our proprietary distributed build system called SN-DBS. All of these problems will be applicable to DTLTO integration with any distributed system in general. The solution for these problems is described in detail in Section 6.
4.1 Regular archives handling
Archive files can be huge. It would be too time-consuming to send the whole archive to a remote node. One of the solutions is to convert regular archives into thin archives and access individual thin archive members.
4.2 Processes access to file system synchronization
Since at any given moment several linker processes can be active on a given machine, they can access the same files at the same time. We need to provide a reliable synchronization mechanism. The existing LLVM file access mutex is not adequate since it does not have a reliable way to detect abnormal process failure.
4.3 File name clashes
We can have situations where file names can clash with each other. We need to provide file name isolation for each individual link target.
4.4 Remote execution is not reliable and can fail at any time
We need to provide a fallback system that can do code generation on a local machine for those modules where remote code generation failed.
5. Linker execution flow
5.1 Linker options
The following options need to be added:
- An option that tells the linker to use the integrated DTLTO approach.
- An option that specifies what kind of distribution system to use.
- Options for debugging and testing.
5.2. Linker SCAN Phase algorithm:
If an input file is a regular archive:
- Convert regular archive into a thin archive. If the regular archive contains another regular archive, it will be converted to a thin archive during the next linker scan pass.
- Replace the path to the regular archive with a path to the thin archive.
After the scan phase has completed, the linker has determined a list of input bitcode modules that will participate in the final link. Also, by now, the linker has collected all symbol resolution information.
5.3. LINK Phase:
The linker uses symbol resolution information for producing individual module summary index files and cross module import lists.
The linker performs code generation on each of the input bitcode modules. This pseudo algorithm depends on the type of job distribution system used.
- Check if any of the input bitcode has a corresponding cache entry. If the cache entry exists, this particular bitcode will be excluded from code generation.
- Generate the build script specific to the job distribution system.
- Invoke the generated build script.
- Check that the list of expected native object files matches the list of the files returned after build script execution. If any of the native object files are missing, the linker uses the fallback system to perform code generation locally for all of these missing native object files.
- Place native object files into corresponding cache entries.
- Perform the final link and produce an executable.
6. Implementation details
6.1 Regular to Thin Archive converter
In section 4.1 we explained why dealing with regular archives is inefficient and proposed converting regular archives into thin archives, later copying only individual thin archive members to remote nodes.
We implemented a regular to thin archive converter based on llvm/Object/Archive.h
- The regular to thin archive converter creates or opens an inter-process sync object.
- It acquires sync object lock.
- It determines to what directory to unpack the regular archive members. This decision is based on the command line option, system temp, or current process directory (in this priority).
- If the thin archive doesn’t exist:
- Unpack the regular archive
- Create the thin archive from regular archive members
- Else:
- Check the thin archive file modification time***
- If (the thin archive is newer than the regular archive) &&*** ( **the thin archive integrity is good):
- Use existing thin archive
- Else:
- Unpack the regular archive
- Create the thin archive from regular archive members.
Note: all thin archive members match regular archive members
6.2 Fallback code generation
In section 4.4 we described a problem that remote execution is not as reliable as local execution and it can fail at any time (e.g. a network is down, remote nodes are not accessible, etc). So, we need to implement a reliable fallback mechanism that can do code generation on a local machine for all those modules that failed to generate remotely.
- Check if a list of missing native object files is not empty.
- Create queue of commands for performing codegen for missing native object files.
- Use the process pool to execute the queue of commands.
- Report fatal error if some native object files are still missing.
6.3 Build script generators
We have created an abstract class that allows adding implementions for build script/makefile generators for different distributed build systems.
We have already added two derived classes that implement an SN-DBS build script generator and an Icecream Makefile generator. If an LLVM contributor would like to add a support for a new distributed build system (e.g. Fast Build), they will have to add an implementation for a derived class for that particular distributed build system, using the classes implemented by us as an example.
6.3.1. Makefile generator
Makefile is used for Distcc, Icecream, Goma, IncrediBuild.
6.3.2. SN-DBS JSON document generator.
SN-DBS is Sony’s proprietary distributed build system.
SN-DBS job description allows multiple link targets in one job.
The job description generator takes a list of input files, creates a corresponding list of commands to perform code generation for each of these files, and writes it to the JSON files for SN-DBS to use.