Repository navigation
[HIPIFY][feature] Local header hipification via Clang PPCallbacks (experimental, opt-in) - Part 1 - #2402
[HIPIFY][feature] Local header hipification via Clang PPCallbacks (experimental, opt-in) - Part 1#2402ranapratap55 wants to merge 10 commits into
Conversation
|
Can we say that the main purpose of this PR is to allow hipifying header files individually as main translation units? |
Yes, correct. The main purpose is to allow hipifying local header files as main translation units. We are doing it by injecting their preceding includes from the parent source file using Clang's This feature is enabled via |
How do we guarantee finding the parent source(s) for a particular header file so we can hipify it as the main translation unit? |
We don't need to discover/guess the parent source file. The user provides it explicitly while hipifying. The main source file passed to hipify-clang is always the parent. For example: |
That is what worries me most.
Technically, the main problem is that we are trying to treat a file, a header file, which is not self-contained, as a source file for hipification (actually, compilation). I want us to think about other approaches here.
|
5e6249f to
353ebef
Compare
As per our discussions/suggestions, I've reworked this PR to implement suggestion 2. I will raise 2 separate PRs as a follow up for:
Updated PR title and description to reflect the changes. |
353ebef to
d10566f
Compare
| cl::opt<bool> OptLocalHeadersRecursive("local-headers-recursive", | ||
| cl::desc("Enable hipification of quoted local headers recursively"), | ||
| cl::opt<bool> SkipLocalHeaders("skip-local-headers", | ||
| cl::desc("Skip implicit hipification of local (quoted) headers included in the main source file"), |
There was a problem hiding this comment.
What if the "local" headers are included with <>?
There was a problem hiding this comment.
I believe this is out of scope. Angled-bracket includes files are resolved using -I search paths. They represent system headers or external library dependencies.
But I do think there is edge case like, if a project uses #include <mylib/utils.h> via -I project/src flag, that would require resolving each angled-bracket against every -I path. That is a meaningful work and requires a separate PR of its own.
There was a problem hiding this comment.
#2505: Angle-bracket local header hipification (#include <>)
| bool collectPrecedingIncludes(const std::string &mainSourceAbspath, | ||
| const std::string &targetHeaderAbspath, | ||
| std::vector<std::string> &outIncludes) { | ||
| std::string mainSourceContent; | ||
| if (!readFile(mainSourceAbspath, mainSourceContent)) { | ||
| errs() << sHipify << sError << "Cannot read source files: " | ||
| << mainSourceAbspath << "\n"; | ||
| return false; | ||
| } | ||
|
|
||
| std::string targetFileName = std::string(sys::path::filename(targetHeaderAbspath)); | ||
| std::istringstream iss(mainSourceContent); | ||
| std::string line; | ||
| std::smatch m; | ||
|
|
||
| while (std::getline(iss, line)) { | ||
| if (std::regex_match(line, m, LocalIncludeRe)) { | ||
| std::string quotedName = m[1].str(); | ||
| std::string quotedFileName = std::string(sys::path::filename(quotedName)); | ||
| if (quotedFileName == targetFileName) | ||
| break; | ||
| std::string absPath; | ||
| if (resolveLocalIncludeInternal(mainSourceAbspath, quotedName, absPath)) | ||
| outIncludes.push_back(absPath); | ||
| } | ||
|
|
||
| if (std::regex_search(line, m, SystemIncludeRe)) { | ||
| outIncludes.push_back(m[1].str()); | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Looks like the collectPrecedingIncludes only scans the immediate parentPath.
What about deeply nested headers?
There was a problem hiding this comment.
yes, correct.
collectPrecedingIncludes scans only one level (the immediate parent). This is intentional for this PR, it was designed for (main.cu → A.h → B.h) where B.h depends on explicitly included before it in A.h and collectPrecedingIncludes(A.h, B.h) finds exactly what B.h needs.
I considered collecting full ancestor chain recursively (for nested headers) but that results in over-injection. When two of those injected headers define same symbol, we get redefinition errors.
I would like to take this (deeply nested headers) as a follow-up PR. We track the full ancestors list and modify from {hdr, parentPath} to {hdr, vector<parentChain>}. And only injects that appear in ancestors(parents) up to the nearest file that explicitly includes the current target. This avoids over-injection issue.
There was a problem hiding this comment.
#2507: Transitive preceding includes for deeply nested recursive headers
d10566f to
7d568b6
Compare
|
Follow up work
|
8d1113a to
8a1adf5
Compare
|
The added tests cover basic recursion and depth-1 injection of “includes”. However, the test that verifies the transitive context is preserved during recursive traversal is still needed. Please add a test that includes a non-self-contained header with a recursion depth of 2 or more. For example, the |
|
I honestly believe we need to preserve |
8a1adf5 to
e198b45
Compare
I've adapted to use
|
Thanks for raising this and it is a fair concern. Based on our earlier discussions, we collectively moved toward implicit-by-default to lower the barrier for users. The current revision already has multiple layers in place for end users like
And I am thinking to add below
With these measures together I believe the implicit behavior is transparent. That said, I am happy to address any specific scenario where this causes harm directly rather than revert the design. What do you think? @emankov |
I still believe that experimental features should stay experimental for at least one release. Meanwhile, it might be thoroughly tested. Next step: inform end users in the documentation that, with the next release, this particular feature will be enabled by default, and for fallback functionality, here are the options to set explicitly. |
|
[IMPORTANT] After reviewing this regex-based solution for header tree calculation, I decided it is not a good solution. Using regex in hipify-perl is a forced decision, but using a regex-based approach with a C++ compiler under the hood can't be treated as a good solution. [Reasons]
[Recommendations]
|
e198b45 to
7d31bff
Compare
Agreed, both
|
Thanks for this feedback. I've removed the entire regex-based include scanning system and replaced with Clang's The new PPCallbacks approach uses We are directly addressing below concerns:
|
| #if LLVM_VERSION_MAJOR < 15 | ||
| const clang::FileEntry *file, | ||
| #elif LLVM_VERSION_MAJOR == 15 | ||
| Optional<clang::FileEntryRef> file, | ||
| #else | ||
| clang::OptionalFileEntryRef file, | ||
| #endif | ||
| StringRef search_path, StringRef relative_path, | ||
| #if LLVM_VERSION_MAJOR < 19 | ||
| const clang::Module *SuggestedModule | ||
| #else | ||
| const clang::Module *SuggestedModule, | ||
| bool ModuleImported | ||
| #endif | ||
| #if LLVM_VERSION_MAJOR > 6 | ||
| , | ||
| clang::SrcMgr::CharacteristicKind FileType | ||
| #endif |
There was a problem hiding this comment.
Might it be simplified?
Did you test it against different LLVM versions?
There was a problem hiding this comment.
Tested by building HIPIFY against LLVM 14, 15, 16, 18 and all compile without any errors. This covers (FileEntry*, Optional<FileEntryRef>, OptionalFileEntryRef, and with/without ModuleImported).
| // Strip the implicit CUDA header that appendArgumentsAdjusters adds — | ||
| // not needed for include scanning and would pollute the results. | ||
| Tool.appendArgumentsAdjuster( | ||
| [](const ct::CommandLineArguments &Args, StringRef) { | ||
| ct::CommandLineArguments filtered; | ||
| for (size_t i = 0; i < Args.size(); ++i) { | ||
| if (Args[i] == "-include" && i + 1 < Args.size() && | ||
| Args[i + 1] == "cuda_runtime.h") { | ||
| ++i; | ||
| continue; | ||
| } | ||
| filtered.push_back(Args[i]); | ||
| } | ||
| return filtered; | ||
| }); |
There was a problem hiding this comment.
Without cuda_runtime.h, Clang lacks base CUDA macros like __CUDACC__ and __device__, causing it to ignore user code hidden behind #ifdef blocks.
There was a problem hiding this comment.
cuda_runtime.h is filtered from the results via the isAngled + isWrittenInMainFile checks.
| extern bool appendArgumentsAdjusters(ct::RefactoringTool &Tool, | ||
| const std::string &sSourceAbsPath, | ||
| const char *hipify_exe); |
There was a problem hiding this comment.
Wrong place to extern. Actually, extern is not needed in LocalHeader.h; it is also misleading.
Instead, better to add a forward function declaration in LocalHeader.cpp.
There was a problem hiding this comment.
Removed the extern and added a local forward declaration of appendArgumentsAdjusters in LocalHeader.cpp instead.
| // CHECK: #include "transitive_parent.h" | ||
| #include <cuda_runtime.h> | ||
| #include <algorithm> | ||
| #include "transitive_parent.h" |
There was a problem hiding this comment.
- The transitive test does not exercise depth 2, in fact. The option
--local-headers-recursiveshould be used instead of--local-headers. - Add the check for
cudaMallocin thetransitive_child.h.
I anticipate this particular test will fail. In that case, the whole algorithm should be revised and fixed (next iteration).
There was a problem hiding this comment.
I've switched to --local-headers-recursive and added the cudaMalloc -> hipMalloc check in transitive_child.h. The test now exercises depth-2, discovery finds transitive_parent.h, then recursed into transitive_child.h. The only fix needed was making transitive_child.h self-contained (#include <algorithm> for std::sort), reason being a recursively-hipified header is compiled standalone.
7d31bff to
3e3db8f
Compare
| bool ok = hipifySingleSource(hdr, hipOut, compDB, OptionsParserPtr, | ||
| hipify_exe, mainSourceAbsPath, false); | ||
| hipify_exe, mainSourceAbsPath, false); |
There was a problem hiding this comment.
It looks like a regression. The context vector is never passed to hipifySingleSource, and the local headers are being hipified in a vacuum, which should lead to a crash.
Additionally, the current tests yield false positives as they are self-contained. A negative test could help here to prove the injection is actually working.
There was a problem hiding this comment.
buildIncludeContext() now derives it from the entries of that same preprocessor run. The ancestor chain's directives that precede the header, oldest first. Ancestors themselves are excluded, since reincluding a parent pulls the header back through its own guard and empties the copy being hipified. The injection also moved from BEGIN to a single END insertion, so it follows -include cuda_runtime.h.
Added a negative test for non self-contained headers. nsc_context.h declares NscVec and is self-contained,nsc_dependent.h uses NscVec and cudaMemset while including nothing. non_self_contained.cu includes them in that order under --local-headers. Because -include cuda_runtime.h is always injected, cudaError_t and cudaMemset resolve on their own. So NscVec is the only symbol that can come from nowhere but the injected context. With an empty vector the header fails with unknown type name NscVec, hipify returns non-zero and the test fails. nsc_dependent.h sits in config.excludes because it cannot carry a RUN.
| if (cmds.empty()) { | ||
| std::string dir = sys::path::parent_path(mainContextPath).str(); | ||
| fallbackDB = std::make_unique<ct::FixedCompilationDatabase>( | ||
| dir, std::vector<std::string>()); | ||
| } |
There was a problem hiding this comment.
Passing an empty vector to FixedCompilationDatabase wipes out all project compiler flags and breaks header resolution for any real-world project with nested include directories.
There was a problem hiding this comment.
Removed the branch entirely and it is unreachable now. Without -p we hold a FixedCompilationDatabase, which synthesizes a command for any path. With -p, loadFromDirectory wraps the JSON database in inferMissingCompileCommands, which borrows the command of the closest matching file, swaps in the filename and drops output args. hipifySingleSource already relies on this, passing a /tmp/*.hip path that's in no database.
| if (!SM.isWrittenInMainFile(hash_loc)) | ||
| return; |
There was a problem hiding this comment.
Using !SM.isWrittenInMainFile(hash_loc) inside the preprocessor callback forcibly prevents Clang from natively traversing nested includes and forces the tool to spawn a brand-new compiler instance for every child header, which destroys macro inheritance. The possible solution here is to remove the check.
There was a problem hiding this comment.
Thanks for this. I've removed the check now and includerPath, isFromMainFile, isSystem are modified as data, so one preprocessor run gives the whole tree. The recursive/non-recursive split is a single filter. Removal alone wasn't ideal though, since CUDA's headers use quoted includes. So paired with SM.isInSystemHeader(HashLoc) and an empty includer path check.
| void ExecuteAction() override { | ||
| clang::CompilerInstance &CI = getCompilerInstance(); | ||
| clang::Preprocessor &PP = CI.getPreprocessor(); | ||
| PP.addPPCallbacks(std::make_unique<IncludeCollectorCallbacks>( | ||
| CI.getSourceManager(), Entries)); | ||
|
|
||
| SmallString<256> norm = in; | ||
| llvm::sys::path::remove_dots(norm, true); | ||
| return llvm::sys::fs::exists(norm); | ||
| } | ||
| PP.EnterMainSourceFile(); | ||
| clang::Token Tok; | ||
| do { | ||
| PP.Lex(Tok); | ||
| } while (Tok.isNot(clang::tok::eof)); | ||
| } |
There was a problem hiding this comment.
Overriding ExecuteAction() to manually call EnterMainSourceFile() and Lex bypasses standard Clang initialization, such as IgnorePragmas(). If a header contains unknown pragmas, your pre-pass might crash.
There was a problem hiding this comment.
Fixed this. IncludeCollectorAction derives from PreprocessOnlyAction and registers the callbacks in BeginSourceFileAction(), so the lexing loop and IgnorePragmas() come from the framework. The old loop was the same code without IgnorePragmas(), so unknown pragmas warned. Collection now propagates Tool.run() failure, a -Werror build would abort on something like #pragma unroll. Known pragmas such as #pragma once are untouched.
| void InclusionDirective(clang::SourceLocation hash_loc, | ||
| const clang::Token &include_token, | ||
| StringRef file_name, bool is_angled, | ||
| clang::CharSourceRange filename_range, | ||
| #if LLVM_VERSION_MAJOR < 15 | ||
| const clang::FileEntry *file, | ||
| #elif LLVM_VERSION_MAJOR == 15 | ||
| Optional<clang::FileEntryRef> file, | ||
| #else | ||
| clang::OptionalFileEntryRef file, | ||
| #endif | ||
| StringRef search_path, StringRef relative_path, | ||
| #if LLVM_VERSION_MAJOR < 19 | ||
| const clang::Module *SuggestedModule | ||
| #else | ||
| const clang::Module *SuggestedModule, | ||
| bool ModuleImported | ||
| #endif | ||
| #if LLVM_VERSION_MAJOR > 6 | ||
| , | ||
| clang::SrcMgr::CharacteristicKind FileType | ||
| #endif |
There was a problem hiding this comment.
Several parameters in InclusionDirective signature are named but never actually used in the function body.
There was a problem hiding this comment.
Fixed in latest code.
7321229 to
7bc6edf
Compare
emankov
left a comment
There was a problem hiding this comment.
Thank you. I do not see any other issues
| ct::RefactoringTool Tool( | ||
| compDB ? *compDB : OptionsParserPtr->getCompilations(), {srcPath}); |
There was a problem hiding this comment.
We need to revise this explicit JSON compilation database detection here and elsewhere, since the getCompilations() database performs it implicitly.
[Explanation]
Situation 1: The workspace has a valid compile_commands.json. When a user runs HIPIFY in a project where the build system (like CMake) has generated a compile_commands.json file, the compDB variable becomes valid (not null). The code initializes the tool using a ternary operator: compDB ? *compDB : OptionsParserPtr->getCompilations(). Because of this operator, the tool strictly prioritizes the raw database (*compDB) and completely bypasses OptionsParserPtr->getCompilations().
Situation 2: The tool attempts to process a local header file (.h, or .hpp, or cuh). By their very nature, compilation databases (compile_commands.json) only record compile commands for translation units (like .cu, .cpp, or .c files). Header files are never recorded in this database because they are not compiled independently.
The Mechanism of Failure:
- HIPIFY discovers a local header and attempts to hipify it.
- The tool queries the raw *compDB database, asking for the specific compile flags for that .h file.
- Because the header file is not listed in the database, the raw database returns nothing.
- Normally, the smart wrapper (CommonOptionsParser) would act as a fallback safety net: it automatically overlays global command-line arguments (like manually passed -I flags) to generate a compile command on the fly for missing files.
- However, because the code forces the use of the raw *compDB, it bypasses this built-in safety net.
- Consequently,
ClangTool::run()finds no command, outputs"Skipping... Compile command not found", and silently aborts.
There was a problem hiding this comment.
I debugged more into this, and I do not think failure mode is reachable. compDB is not a raw database. main.cpp:393 calls CompilationDatabase::loadFromDirectory then reaches JSONCompilationDatabasePlugin::loadFromDirectory and returns inferTargetAndDriverMode(inferMissingCompileCommands((...))). The comment at line 159 says it also infers compile commands for files not present in the database. So the header inference you're describing is already inside *compDB.
getCompilations() is that same object plus ArgumentsAdjustingCompilations, which rewrites commands the inner database already returned and so cannot synthesize one for a missing file. Our /unit_tests/compilation_database/ tests show it is working. They run with -p= and hipify a /tmp/*.hip path that is in no JSON. That is the interpolating wrapper answering through *compDB.
You are right that the ternary redundant. The compDB branch drops --extra-arg, and the JSON is parsed twice per run. So I'd rather fix it as a separate NFC PR, with a -p plus --local-headers test as the first commit, since no combination covers it as of today.
…jection via ArgumentsAdjusters
…and restore opt-in flags
7bc6edf to
a82777b
Compare
Problem Statement
hipify-clangcurrently only transforms the main source file passed as input. Local (quoted) headers included in the main file remain same as CUDA code. These headers often cannot be hipified individually because they are not self-contained headers which requires types, operators, and definitions from preceding includes in the parent source.Example: dxtc.cu includes
CudaMath.h, which usesfloat3operators (+=,*,-) defined inhelper_math.h. HipifyingCudaMath.halone fails because these operators are unavailable. Similar cases:Solution
Two new opt-in flags enable local header hipification:
The feature is experimental and disabled by default. It will be promoted to default behavior in later release cycles with
--skip-local-headersas the opt-out fallback.Supersedes #2275
Summary of changes in this PR
Clang PPCallbacks instead of regex
Include discovery is using Clang's
PPCallbacks::InclusionDirectivevia preprocessing pre-pass, not regex. This correctly handles conditional compilation (#ifdef), macro-computed includes (#include MACRO), compiler search paths (-I,-isystem), and comments.Fixed Compilation Database fallback
Header files which have no entry in
compile_commands.jsonfalls back to aFixedCompilationDatabaserooted at the main source file's directory so that relative paths resolve correctly.Cycle and duplicate detection
A
processedset prevents re-hipification, and aqueuedset prevents duplicate work-queue entries during recursive traversal.ToDo work (follow-up PRs):
--skip-local-headers)#include <mylib/utils.h>via-I) — [HIPIFY][#2402] hipify project-local headers included with angle brackets (<>) #2505