You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have added this issue to motivate the current design of the PR and solicit feedback on certain points.
Problem
RAFT is a heavily templated library. Several core functions are very expensive to compile and we want to prevent duplicate compilation of this functionality. To limit build time, RAFT provides a precompiled library (libraft.so) where expensive function templates are instantiated for the most commonly used template parameters. This is not a complete solution, however, due to two problems (1) accidental reinstantiations and (2) unnecessary dependencies:
In practice, it often happens that a function template is accidentally reinstantiated, even when it is already provided by the library. This can have a big negative impact on compile times. It happens in consuming libraries, like cuML, but is also prevalent within libraft.so itself [#1358, #1360].
A minor change in the core implementation of a function often causes the recompilation of dependent translation units. A particularly bad example is neighbors/specializations.cuh, which provides the extern template instantiations for many expensive kernels and is also included over 100 times in the RAFT project. Any change to any of these kernels triggers a rebuild of all 100 files that include the specializations header.
Requirements
A solution to these two problems should satisfy the following requirements:
An accidental reinstantiation must be impossible when compiling libraft.so, i.e., it must result in an error.
In case of an error, the solution must result in useful and actionable error messages. Specifically, it should not introduce linker errors during development (they only fire at the very end of compilation and do not point to the source location that introduced the error).
It must be possible for downstream libraries to continue to use RAFT as a header-only library, as well as a precompiled library.
In addition, there are some nice-to-haves. That is, we obviously want to write as little boilerplate code as possible. Also, it would be nice if downstream code would not have to bother with the following if else preprocessor logic when including a header:
#if defined RAFT_COMPILED
#include<raft_runtime/distance/pairwise_distance.hpp>
#else
#include<raft/distance/distance.cuh>
#endif
Background: C++ templates
I include some background on C++ function templates here, feel free to skip and refer back in case of questions.
A function template in C++ can be declared, defined, and instantiated. In addition, there is something called template specialization but this is not used in RAFT (the “specializations” directory currently contains template instantiations; a case of unfortunate naming).
A template instantiation can be explicit, extern explicit, or implicit. An explicit instantiation looks very similar to a function template declaration. Instead of having the angle brackets in front of the function name, however, the angle brackets are placed behind the function name. When an explicit template instantiation is encountered, the compiler will generate code for the template instance. To prevent generating code, an extern template instantiation can be used, which tells the compiler that some other translation unit already contains the code. An implicit instantiation is an instantiation of a function by use, e.g., a function call, taking a reference, etc.
Examples of declaration, definition, and both types of instantiation are shown below:
An important difference between implicit and explicit instantiation is that explicit instantiation requires that the template has been defined (the function body is needed to generate code). An implicit instance only requires that the template has been declared: instead of generating code the compiler will insert a linker directive. If the template is not instantiated anywhere else, this will result in linker errors.
If the compiler encounters an extern template instantiation and an explicit instantiation, the explicit instantiation “wins”: the code for the template is generated and ends up in the translation unit.
The key to the proposed solution consists of (1) listing allowed instantiations using explicit template instantiation and (2) controlling exactly when and where implicit template instantiation is allowed (i.e., a non-listed instantiation). Part (1) of this solution is already implemented in the specializations headers in RAFT, part (2) would allow implicit instantiations in the following situations:
When compiling libraft.so: expensive function templates are explicitly instantiated and their implicit instantiation results in a compiler error.
When compiling an external package that depends on libraft.so: implicit instantiation is allowed and when an extern template instantiation is available it is used (code generation is skipped).
When compiling with header-only RAFT: implicit instantiation is allowed and extern template instantiations are unavailable.
Solution
What
This section describes (1) macros that are used to control template instantiation, (2) a structure for organizing header files, and (3) a mechanism for meaningful error messages in case of accidental template instantiation.
Macros. We define the macros RAFT_COMPILED and RAFT_EXPLICIT_INSTANTIATE during compilation of libraft.so and the tests and benchmarks. The RAFT_COMPILED is already used in RAFT and indicates if a translation unit will be linked with libraft.so. It is currently marked in CMakeLists.txt as INTERFACE, meaning it is defined only for downstream libraries using RAFT. In this proposal, it is marked PUBLIC meaning it will be defined both for compiling RAFT as well as downstream libraries.
The RAFT_EXPLICIT_INSTANTIATE macro is new and is only defined during compilation of libraft.so itself (i.e. PRIVATE). When defined, it indicates that implicit instantiations of expensive function templates are forbidden (they should result in a compiler error).
# RAFT_COMPILED is set during compilation of libraft.so as well as downstream
# libraries (due to "PUBLIC")
target_compile_definitions(raft_lib PUBLIC"RAFT_COMPILED")
# RAFT_EXPLICIT_INSTANTIATE is set during compilation of libraft.so (due to
# "PRIVATE")
target_compile_definitions(raft_lib PRIVATE"RAFT_EXPLICIT_INSTANTIATE")
Header organization. Any header file that defines an expensive function template (say expensive.cuh) should be split in three parts: expensive.cuh, expensive-inl.cuh, and expensive-ext.cuh. The file expensive-inl.cuh (“inl” for “inline”) contains the original template definitions and remains largely unchanged. The file expensive.cuh inludes one or both of the other two files, depending on the values of the RAFT_COMPILED and RAFT_EXPLICIT_INSTANTIATE macros. The file expensive-ext.cuh contains the external template instantiations. In addition, if RAFT_EXPLICIT_INSTANTIATE is set, it contains template defitions that ensure that a compiler error is raised when a function template is implicitly instantiated.
The dispatching by expensive.cuh is performed as follows:
#if !defined(RAFT_EXPLICIT_INSTANTIATE)
// If implicit instantiation is allowed, include template definitions.
#include"expensive-inl.cuh"
#endif
#ifdef RAFT_COMPILED
// Include extern template instantiations when RAFT is compiled.
#include"expensive-ext.cuh"
#endif
The file expensive-in.cuh is unchanged:
namespaceraft {
template <typename T>
voidexpensive(T arg) {
// .. function body
}
} // namespace raft
The file expensive-ext.cuh contains the following:
First, if RAFT_EXPLICIT_INSTANTIATE is set, expensive is defined. This is necessary, because the definition in expensive-inl.cuh was skipped. The macro RAFT_EXPLICIT defines the function body: this macro ensures that an informative error message is generated when an implicit instantiation erroneously occurs and is described below. Finally, the extern template instantiations are listed.
To actually generate the code for the template instances, the file expensive.cu contains the following. Note that the only difference between the extern template instantiations in expensive-ext.cuh and these lines are the removal of the word extern:
Error messages. Function templates that should be explicitly instantiated are tagged with the RAFT_EXPLICIT macro(link). This macro defines a function body that triggers an error when the template is accidentally instantiated. The error message looks like this:
static assertion failed with "ACCIDENTAL_IMPLICIT_INSTANTIATION
If you see this error, then you have implicitly instantiated a function
template. To keep compile times in check, libraft has the policy of
explicitly instantiating templates. To fix the compilation error, follow
these steps.
If you scroll up or down a bit in your error message, you probably saw a line
like the following:
detected during instantiation of "void raft::foo(T) [with T=float]" at line [..]
Simplest temporary solution:
Add '#undef RAFT_EXPLICIT_INSTANTIATE' at the top of your .cpp/.cu file.
Best solution:
1. Add the following line to the file include/raft/foo.hpp:
extern template void raft::foo<double>(double);
2. Add the following line to the file src/raft/foo.cpp:
template void raft::foo<double>(double)
"
detected during instantiation of "void raft::foo(T) [with T=int]" at line 115
This should help a user who just wants the error to go away while prototyping some new functionality (they will have more than enough time to circle back to this issue while the code is compiling). In addition, it tells the user how to fix the issue the “right way” by adding explicit instantiations to the correct files.
Why/how does this solve the problem
I address why this solution prevents accidental reinstantiation and unnecessary dependencies in three situations: incremental compilation with tests and/or benchmarks, compilation in RAFT CI, compilation of downstream libraries.
Incremental compilation. In this situation, libraft.so, the tests, and benchmarks are compiled with RAFT_COMPILED and RAFT_EXPLICIT_INSTANTIATE defined. Therefore, when a template is instantiated that is not in the list of explicit extern instantiations, it will raise a compiler error.
When the internals of a kernel in expensive-inl.cuh are changed, the test file will not have to be recompiled since it only depends on expensive.cuh and expensive-ext.cuh. Only the file expensive.cu will be recompiled. This prevents chains of recompilations from forming as a result of a simple change.
Compilation in CI. Here, we can also catch accidental implicit instantiations. In addition, as a result of reducing unnecessary dependencies, the probability that sccache has a cache hit is drastically increased, since changes to the internals of expensive function templates do not lead to a cascade of recompilation anymore.
Compilation of downstream libraries. Downstream libraries can either use header-only RAFT, in which case nothing changes (both macros are undefined and the *-inl.cuh headers are included), or they can use libraft.so in which case RAFT_COMPILED is defined. This way, when the library uses a template instance that has already been compiled in libraft.so, code generation is skipped and the code in libraft.so is used. When the library instantiates a function template that has not been instantiated in libraft.so, the code is generated like it was before without any errors. The library might want to opt-in to implicit instantiation prevention by defining RAFT_EXPLICIT_INSTANTIATE, but I expect this will be rare.
Advantages / disadvantages
First of all, this proposal solves the problems of accidental reinstantiation and unnecessary dependencies. It therefore substantially cuts down on compilation times and we can count on this being the case in the future.
In terms of code organization, an additional advantage is that things are easier to find. The current directory structure of specializations does not follow the directory structure of include/ and it can be a bit difficult to find an extern template instantiation. The proposed organization always has extern template instantiations in the *-ext.cuh header and the actual instantiations in the src/ directory in the same relative directory.
A disadvantage is that the proposed structure requires more boilerplate, as we have four repetitions of the function template declaration instead of three. This can be bit annoying to set up, but it only has to be done for new functionality or when the interface of a function changes. This will happen more rarely than changing the internals of a function, which should now be considerably faster.
Nuances
Some kernels are instantiated many times, but it may not make sense to actually prevent implicit instantiation. One case is raft::linalg::reduce that is used in many places. To cover all instances with an extern template instantation would be prohibitive, but covering none would result in some instances being defined in over 80 translation units. In this case, it would make sense to allow implicit instantiations and also have extern template instantiations for commonly used types.
The RAFT_EXPLICIT macro only works reliably for functions. In the detail headers, there were some cases where a struct was used to dispatch to different kernels. In these cases, I had to replace a member function by a free function.
Alternatives considered
One alternative that would require less boilerplate is to declare instead of define function templates. A downside is that it would result in linker errors but not raise any compiler errors on implicit instantiation (and thus would not tell you where the instantiation occurred).
Another alternative is to continue with the current strategy. A downside is that it will require constant firefighting to ensure that (1) extern template instantiations are used when they are available, (2) index types and data types at usage site do not start to diverge from the extern template instantiations. All of this would have to be done based on the build time reports (an increase in build time being an indication that something is wrong) and copious use of cuobjdump to hunt for duplicate template instantiations. So far, experience has shown that this is a brittle strategy.
Open questions
Would it be worth developing a strategy for forcing explicit instantiation of structs as well? They are sometimes used internally and if we can it to work it would save quite some typing, but I fear the solution would look complicated (even compared to the current proposal).
Where to keep the docstrings? In Remove specializations and split expensive headers #1415, I have mostly opted to keep docstrings in both `-ext` and `-inl` headers. I would lean towards having the docstrings in the `-ext` headers, as those headers are more dense and do not show the function bodies, which can give a nicer overview.
This issue describes problems we currently have with using specialization headers to reduce compile times and proposes a fix.
This issue accompanies #1415
I have added this issue to motivate the current design of the PR and solicit feedback on certain points.
Problem
RAFT is a heavily templated library. Several core functions are very expensive to compile and we want to prevent duplicate compilation of this functionality. To limit build time, RAFT provides a precompiled library (
libraft.so) where expensive function templates are instantiated for the most commonly used template parameters. This is not a complete solution, however, due to two problems (1) accidental reinstantiations and (2) unnecessary dependencies:In practice, it often happens that a function template is accidentally reinstantiated, even when it is already provided by the library. This can have a big negative impact on compile times. It happens in consuming libraries, like cuML, but is also prevalent within
libraft.soitself [#1358, #1360].A minor change in the core implementation of a function often causes the recompilation of dependent translation units. A particularly bad example is
neighbors/specializations.cuh, which provides the extern template instantiations for many expensive kernels and is also included over 100 times in the RAFT project. Any change to any of these kernels triggers a rebuild of all 100 files that include the specializations header.Requirements
A solution to these two problems should satisfy the following requirements:
An accidental reinstantiation must be impossible when compiling
libraft.so, i.e., it must result in an error.In case of an error, the solution must result in useful and actionable error messages. Specifically, it should not introduce linker errors during development (they only fire at the very end of compilation and do not point to the source location that introduced the error).
It must be possible for downstream libraries to continue to use RAFT as a header-only library, as well as a precompiled library.
In addition, there are some nice-to-haves. That is, we obviously want to write as little boilerplate code as possible. Also, it would be nice if downstream code would not have to bother with the following if else preprocessor logic when including a header:
Background: C++ templates
I include some background on C++ function templates here, feel free to skip and refer back in case of questions.
A function template in C++ can be declared, defined, and instantiated. In addition, there is something called template specialization but this is not used in RAFT (the “specializations” directory currently contains template instantiations; a case of unfortunate naming).
A template instantiation can be explicit, extern explicit, or implicit. An explicit instantiation looks very similar to a function template declaration. Instead of having the angle brackets in front of the function name, however, the angle brackets are placed behind the function name. When an explicit template instantiation is encountered, the compiler will generate code for the template instance. To prevent generating code, an extern template instantiation can be used, which tells the compiler that some other translation unit already contains the code. An implicit instantiation is an instantiation of a function by use, e.g., a function call, taking a reference, etc.
Examples of declaration, definition, and both types of instantiation are shown below:
An important difference between implicit and explicit instantiation is that explicit instantiation requires that the template has been defined (the function body is needed to generate code). An implicit instance only requires that the template has been declared: instead of generating code the compiler will insert a linker directive. If the template is not instantiated anywhere else, this will result in linker errors.
If the compiler encounters an extern template instantiation and an explicit instantiation, the explicit instantiation “wins”: the code for the template is generated and ends up in the translation unit.
The key to the proposed solution consists of (1) listing allowed instantiations using explicit template instantiation and (2) controlling exactly when and where implicit template instantiation is allowed (i.e., a non-listed instantiation). Part (1) of this solution is already implemented in the specializations headers in RAFT, part (2) would allow implicit instantiations in the following situations:
When compiling
libraft.so: expensive function templates are explicitly instantiated and their implicit instantiation results in a compiler error.When compiling an external package that depends on
libraft.so: implicit instantiation is allowed and when an extern template instantiation is available it is used (code generation is skipped).When compiling with header-only RAFT: implicit instantiation is allowed and extern template instantiations are unavailable.
Solution
What
This section describes (1) macros that are used to control template instantiation, (2) a structure for organizing header files, and (3) a mechanism for meaningful error messages in case of accidental template instantiation.
Macros. We define the macros
RAFT_COMPILEDandRAFT_EXPLICIT_INSTANTIATEduring compilation oflibraft.soand the tests and benchmarks. TheRAFT_COMPILEDis already used in RAFT and indicates if a translation unit will be linked withlibraft.so. It is currently marked inCMakeLists.txtasINTERFACE, meaning it is defined only for downstream libraries using RAFT. In this proposal, it is markedPUBLICmeaning it will be defined both for compiling RAFT as well as downstream libraries.The
RAFT_EXPLICIT_INSTANTIATEmacro is new and is only defined during compilation oflibraft.soitself (i.e.PRIVATE). When defined, it indicates that implicit instantiations of expensive function templates are forbidden (they should result in a compiler error).Header organization. Any header file that defines an expensive function template (say
expensive.cuh) should be split in three parts:expensive.cuh,expensive-inl.cuh, andexpensive-ext.cuh. The fileexpensive-inl.cuh(“inl” for “inline”) contains the original template definitions and remains largely unchanged. The fileexpensive.cuhinludes one or both of the other two files, depending on the values of theRAFT_COMPILEDandRAFT_EXPLICIT_INSTANTIATEmacros. The fileexpensive-ext.cuhcontains the external template instantiations. In addition, ifRAFT_EXPLICIT_INSTANTIATEis set, it contains template defitions that ensure that a compiler error is raised when a function template is implicitly instantiated.The dispatching by
expensive.cuhis performed as follows:The file
expensive-in.cuhis unchanged:The file
expensive-ext.cuhcontains the following:First, if
RAFT_EXPLICIT_INSTANTIATEis set,expensiveis defined. This is necessary, because the definition inexpensive-inl.cuhwas skipped. The macroRAFT_EXPLICITdefines the function body: this macro ensures that an informative error message is generated when an implicit instantiation erroneously occurs and is described below. Finally, the extern template instantiations are listed.To actually generate the code for the template instances, the file
expensive.cucontains the following. Note that the only difference between the extern template instantiations inexpensive-ext.cuhand these lines are the removal of the wordextern:Error messages. Function templates that should be explicitly instantiated are tagged with the
RAFT_EXPLICITmacro(link). This macro defines a function body that triggers an error when the template is accidentally instantiated. The error message looks like this:This should help a user who just wants the error to go away while prototyping some new functionality (they will have more than enough time to circle back to this issue while the code is compiling). In addition, it tells the user how to fix the issue the “right way” by adding explicit instantiations to the correct files.
Why/how does this solve the problem
I address why this solution prevents accidental reinstantiation and unnecessary dependencies in three situations: incremental compilation with tests and/or benchmarks, compilation in RAFT CI, compilation of downstream libraries.
Incremental compilation. In this situation, libraft.so, the tests, and benchmarks are compiled with
RAFT_COMPILEDandRAFT_EXPLICIT_INSTANTIATEdefined. Therefore, when a template is instantiated that is not in the list of explicit extern instantiations, it will raise a compiler error.When the internals of a kernel in
expensive-inl.cuhare changed, the test file will not have to be recompiled since it only depends onexpensive.cuhandexpensive-ext.cuh. Only the fileexpensive.cuwill be recompiled. This prevents chains of recompilations from forming as a result of a simple change.Compilation in CI. Here, we can also catch accidental implicit instantiations. In addition, as a result of reducing unnecessary dependencies, the probability that
sccachehas a cache hit is drastically increased, since changes to the internals of expensive function templates do not lead to a cascade of recompilation anymore.Compilation of downstream libraries. Downstream libraries can either use header-only RAFT, in which case nothing changes (both macros are undefined and the
*-inl.cuhheaders are included), or they can uselibraft.soin which caseRAFT_COMPILEDis defined. This way, when the library uses a template instance that has already been compiled inlibraft.so, code generation is skipped and the code inlibraft.sois used. When the library instantiates a function template that has not been instantiated inlibraft.so, the code is generated like it was before without any errors. The library might want to opt-in to implicit instantiation prevention by definingRAFT_EXPLICIT_INSTANTIATE, but I expect this will be rare.Advantages / disadvantages
First of all, this proposal solves the problems of accidental reinstantiation and unnecessary dependencies. It therefore substantially cuts down on compilation times and we can count on this being the case in the future.
In terms of code organization, an additional advantage is that things are easier to find. The current directory structure of
specializationsdoes not follow the directory structure ofinclude/and it can be a bit difficult to find an extern template instantiation. The proposed organization always has extern template instantiations in the*-ext.cuhheader and the actual instantiations in thesrc/directory in the same relative directory.A disadvantage is that the proposed structure requires more boilerplate, as we have four repetitions of the function template declaration instead of three. This can be bit annoying to set up, but it only has to be done for new functionality or when the interface of a function changes. This will happen more rarely than changing the internals of a function, which should now be considerably faster.
Nuances
Some kernels are instantiated many times, but it may not make sense to actually prevent implicit instantiation. One case is
raft::linalg::reducethat is used in many places. To cover all instances with an extern template instantation would be prohibitive, but covering none would result in some instances being defined in over 80 translation units. In this case, it would make sense to allow implicit instantiations and also have extern template instantiations for commonly used types.The
RAFT_EXPLICITmacro only works reliably for functions. In thedetailheaders, there were some cases where a struct was used to dispatch to different kernels. In these cases, I had to replace a member function by a free function.Alternatives considered
One alternative that would require less boilerplate is to declare instead of define function templates. A downside is that it would result in linker errors but not raise any compiler errors on implicit instantiation (and thus would not tell you where the instantiation occurred).
Another alternative is to continue with the current strategy. A downside is that it will require constant firefighting to ensure that (1) extern template instantiations are used when they are available, (2) index types and data types at usage site do not start to diverge from the extern template instantiations. All of this would have to be done based on the build time reports (an increase in build time being an indication that something is wrong) and copious use of
cuobjdumpto hunt for duplicate template instantiations. So far, experience has shown that this is a brittle strategy.Open questions
Would it be worth developing a strategy for forcing explicit instantiation of structs as well? They are sometimes used internally and if we can it to work it would save quite some typing, but I fear the solution would look complicated (even compared to the current proposal).
Where to keep the docstrings? In Remove specializations and split expensive headers #1415, I have mostly opted to keep docstrings in both `-ext` and `-inl` headers. I would lean towards having the docstrings in the `-ext` headers, as those headers are more dense and do not show the function bodies, which can give a nicer overview.