[analyzer] Recognize missing MPI collectives in MPI-Checker#209263
[analyzer] Recognize missing MPI collectives in MPI-Checker#209263steakhal wants to merge 1 commit into
Conversation
Fixes a false positive in the optin.mpi.MPI-Checker where legitimate code
was flagged with "Request 'X' has no matching nonblocking call."
Background for reviewers unfamiliar with MPI or this checker
------------------------------------------------------------
MPI (Message Passing Interface) is the standard API for communication
between processes in HPC/cluster programs (C/C++/Fortran). Many MPI
operations come in a *nonblocking* form, prefixed with "I" (for
"immediate"): e.g. MPI_Isend, MPI_Iscatter. A nonblocking call returns
immediately and hands back an MPI_Request handle; the caller must later
complete it with MPI_Wait (or MPI_Waitall). Forgetting to wait, or reusing
a request before waiting, are real bugs (buffer reuse races, leaked
requests).
The MPI-Checker (originally contributed as a GSoC 2016 project, and is a
domain-specific check - hence it's in "optin").
It tracks each MPI_Request region through a small state machine and reports:
three misuse patterns:
* **missing wait**: a nonblocking call whose request is never waited on
* **double nonblocking**: a request reused by a second nonblocking call
before being waited on
* **unmatched wait**: MPI_Wait on a request with no originating
nonblocking call
Root cause
----------
`MPIFunctionClassifier` maintains a hand-written list of known MPI functions.
That list was incomplete: several standard collectives -- notably the
vector variants (`MPI_Iscatterv`, `MPI_Igatherv`, ...), the scans, the
reduce-scatter family, and `MPI_Ibarrier` -- were entirely absent. Because
`isNonBlockingType()` returned `false` for them, `checkDoubleNonblocking()`
never recorded the request as Nonblocking, so the matching `MPI_Wait` looked
unmatched and produced a FP.
Note that only `isNonBlockingType()`/`isWaitType()`/`isMPI_Wait()`/`isMPI_Waitall()`
actually drive the checker; the per-shape predicates (`isScatterType`,
`isGatherType`, `isCollToColl`, etc.) and their backing containers are currently
unused. `checkDoubleNonblocking()` also reads the request from the *last*
argument, which is generic since every nonblocking MPI function places
`MPI_Request*` last -- so no per-function argument handling is needed.
Fix
---
Register the remaining standard collectives and mark the nonblocking ("I")
variants as nonblocking:
* **vector variants**: `MPI_Scatterv`/`Iscatterv`, `MPI_Gatherv`/`Igatherv`,
`MPI_Allgatherv`/`Iallgatherv`, `MPI_Alltoallv`/`Ialltoallv`,
`MPI_Alltoallw`/`Ialltoallw`
* **nonblocking barrier**: `MPI_Ibarrier`
* **scans**: `MPI_Scan`/`Iscan`, `MPI_Exscan`/`Iexscan`
* **reduce-scatter**: `MPI_Reduce_scatter`/`Ireduce_scatter`,
`MPI_Reduce_scatter_block`/`Ireduce_scatter_block`
These functions are not referenced by any per-shape predicate, so they are
added directly to the classification containers without a dedicated
`IdentifierInfo` member. Neighborhood collectives (`MPI_Ineighbor_*`) and the
wait-family beyond `Wait`/`Waitall` (`Waitany`/`Waitsome`/`Test*`)
remain out of scope.
Adds regression tests covering the reported repro (no warning), and confirms
double-nonblocking and missing-wait are still detected for the new functions.
Fixes llvm#208107
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
@llvm/pr-subscribers-clang @llvm/pr-subscribers-clang-static-analyzer-1 Author: Balázs Benics (steakhal) ChangesFixes a false positive in the Background for reviewers unfamiliar with MPI or this checkerMPI (Message Passing Interface) is the standard API for communication between processes in HPC/cluster programs (C/C++/Fortran). Many MPI operations come in a nonblocking form, prefixed with "I" (for "immediate"): e.g. The MPI-Checker (originally contributed as a GSoC 2016 project, and is a domain-specific check - hence it's in "optin").
Root cause
Note that only FixRegister the remaining standard collectives and mark the nonblocking ("I") variants as nonblocking:
These functions are not referenced by any per-shape predicate, so they are added directly to the classification containers without a dedicated Adds regression tests covering the reported repro (no warning), and confirms double-nonblocking and missing-wait are still detected for the new functions. Fixes #208107 Full diff: https://github.com/llvm/llvm-project/pull/209263.diff 3 Files Affected:
diff --git a/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp b/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
index 277b3ed2e1056..2d618c19b191f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
@@ -174,6 +174,48 @@ void MPIFunctionClassifier::initCollectiveIdentifiers(ASTContext &ASTCtx) {
MPINonBlockingTypes.push_back(IdentInfo_MPI_Ialltoall);
MPIType.push_back(IdentInfo_MPI_Ialltoall);
assert(IdentInfo_MPI_Ialltoall);
+
+ // Register the remaining standard collectives. These are not referenced by
+ // any of the individual is<Kind>Type() predicates, so they don't need a
+ // dedicated IdentifierInfo member; the checker only relies on them being
+ // classified as collective / nonblocking. All nonblocking (MPI_I...)
+ // variants take an MPI_Request as their last argument.
+ auto AddCollective = [&](StringRef Name) {
+ IdentifierInfo *II = &ASTCtx.Idents.get(Name);
+ MPICollectiveTypes.push_back(II);
+ MPIType.push_back(II);
+ return II;
+ };
+ auto AddNonBlockingCollective = [&](StringRef Name) {
+ MPINonBlockingTypes.push_back(AddCollective(Name));
+ };
+
+ // Vector ("v"/"w") variants of the collectives above.
+ AddCollective("MPI_Scatterv");
+ AddNonBlockingCollective("MPI_Iscatterv");
+ AddCollective("MPI_Gatherv");
+ AddNonBlockingCollective("MPI_Igatherv");
+ AddCollective("MPI_Allgatherv");
+ AddNonBlockingCollective("MPI_Iallgatherv");
+ AddCollective("MPI_Alltoallv");
+ AddNonBlockingCollective("MPI_Ialltoallv");
+ AddCollective("MPI_Alltoallw");
+ AddNonBlockingCollective("MPI_Ialltoallw");
+
+ // Nonblocking barrier.
+ AddNonBlockingCollective("MPI_Ibarrier");
+
+ // Prefix reductions (scans).
+ AddCollective("MPI_Scan");
+ AddNonBlockingCollective("MPI_Iscan");
+ AddCollective("MPI_Exscan");
+ AddNonBlockingCollective("MPI_Iexscan");
+
+ // Reduce-scatter family.
+ AddCollective("MPI_Reduce_scatter");
+ AddNonBlockingCollective("MPI_Ireduce_scatter");
+ AddCollective("MPI_Reduce_scatter_block");
+ AddNonBlockingCollective("MPI_Ireduce_scatter_block");
}
void MPIFunctionClassifier::initAdditionalIdentifiers(ASTContext &ASTCtx) {
diff --git a/clang/test/Analysis/MPIMock.h b/clang/test/Analysis/MPIMock.h
index 01d2d42fc58a3..f3bc0676d132b 100644
--- a/clang/test/Analysis/MPIMock.h
+++ b/clang/test/Analysis/MPIMock.h
@@ -37,6 +37,7 @@ namespace std { template<class T> struct complex { T real; T imag; }; }
#define MPI_COMM_WORLD 0
#define MPI_STATUS_IGNORE 0
#define MPI_STATUSES_IGNORE 0
+#define MPI_REQUEST_NULL 0
#define MPI_SUM 0
// mock functions
@@ -53,3 +54,9 @@ int MPI_Reduce(const void *, void *, int, MPI_Datatype, MPI_Op, int, MPI_Comm);
int MPI_Ireduce(const void *, void *, int, MPI_Datatype, MPI_Op, int, MPI_Comm,
MPI_Request *);
int MPI_Bcast(void *, int count, MPI_Datatype, int, MPI_Comm);
+int MPI_Iscatterv(const void *, const int[], const int[], MPI_Datatype, void *,
+ int, MPI_Datatype, int, MPI_Comm, MPI_Request *);
+int MPI_Iscan(const void *, void *, int, MPI_Datatype, MPI_Op, MPI_Comm,
+ MPI_Request *);
+int MPI_Ireduce_scatter(const void *, void *, const int[], MPI_Datatype,
+ MPI_Op, MPI_Comm, MPI_Request *);
diff --git a/clang/test/Analysis/mpichecker.cpp b/clang/test/Analysis/mpichecker.cpp
index f7644520ebcf5..f2fd6c0951a2f 100644
--- a/clang/test/Analysis/mpichecker.cpp
+++ b/clang/test/Analysis/mpichecker.cpp
@@ -48,6 +48,46 @@ void matchedWait3() {
}
} // no error
+void matchedWaitIscatterv() {
+ double buf = 0;
+ MPI_Request req = MPI_REQUEST_NULL;
+ MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+ 0, MPI_COMM_WORLD, &req);
+ MPI_Wait(&req, MPI_STATUS_IGNORE);
+} // no error: this used to report a FP before GH208107.
+
+void missingWaitIscatterv() {
+ double buf = 0;
+ MPI_Request req = MPI_REQUEST_NULL;
+ MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+ 0, MPI_COMM_WORLD, &req);
+} // expected-warning {{Request 'req' has no matching wait.}}
+
+void doubleNonblockingIscatterv() {
+ double buf = 0;
+ MPI_Request req = MPI_REQUEST_NULL;
+ MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+ 0, MPI_COMM_WORLD, &req);
+ // expected-warning@+1 {{Double nonblocking on request 'req'.}}
+ MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+ 0, MPI_COMM_WORLD, &req);
+ MPI_Wait(&req, MPI_STATUS_IGNORE);
+}
+
+void matchedWaitIscan() {
+ double buf = 0;
+ MPI_Request req = MPI_REQUEST_NULL;
+ MPI_Iscan(MPI_IN_PLACE, &buf, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD, &req);
+ MPI_Wait(&req, MPI_STATUS_IGNORE);
+} // no error
+
+void missingWaitIreduceScatter() {
+ double buf = 0;
+ MPI_Request req = MPI_REQUEST_NULL;
+ MPI_Ireduce_scatter(MPI_IN_PLACE, &buf, NULL, MPI_DOUBLE, MPI_SUM,
+ MPI_COMM_WORLD, &req);
+} // expected-warning {{Request 'req' has no matching wait.}}
+
void missingWait1() { // Check missing wait for dead region.
double buf = 0;
MPI_Request sendReq1;
|
|
This is practically vibe-coded and I don't really want to get myself into MPI. |
Fixes a false positive in the
optin.mpi.MPI-Checkerwhere legitimate code was flagged with "Request 'X' has no matching nonblocking call."Background for reviewers unfamiliar with MPI or this checker
MPI (Message Passing Interface) is the standard API for communication between processes in HPC/cluster programs (C/C++/Fortran). Many MPI operations come in a nonblocking form, prefixed with "I" (for "immediate"): e.g.
MPI_Isend,MPI_Iscatter. A nonblocking call returns immediately and hands back anMPI_Requesthandle; the caller must later complete it withMPI_Wait(orMPI_Waitall). Forgetting to wait, or reusing a request before waiting, are real bugs (buffer reuse races, leaked requests).The MPI-Checker (originally contributed as a GSoC 2016 project, and is a domain-specific check - hence it's in "optin").
It tracks each
MPI_Requestregion through a small state machine and reports: three misuse patterns:MPI_Waiton a request with no originating nonblocking callRoot cause
MPIFunctionClassifiermaintains a hand-written list of known MPI functions. That list was incomplete: several standard collectives -- notably the vector variants (MPI_Iscatterv,MPI_Igatherv, ...), the scans, the reduce-scatter family, andMPI_Ibarrier-- were entirely absent. BecauseisNonBlockingType()returnedfalsefor them,checkDoubleNonblocking()never recorded the request as Nonblocking, so the matchingMPI_Waitlooked unmatched and produced a FP.Note that only
isNonBlockingType()/isWaitType()/isMPI_Wait()/isMPI_Waitall()actually drive the checker; the per-shape predicates (isScatterType,isGatherType,isCollToColl, etc.) and their backing containers are currently unused.checkDoubleNonblocking()also reads the request from the last argument, which is generic since every nonblocking MPI function placesMPI_Request*last -- so no per-function argument handling is needed.Fix
Register the remaining standard collectives and mark the nonblocking ("I") variants as nonblocking:
MPI_Scatterv/Iscatterv,MPI_Gatherv/Igatherv,MPI_Allgatherv/Iallgatherv,MPI_Alltoallv/Ialltoallv,MPI_Alltoallw/IalltoallwMPI_IbarrierMPI_Scan/Iscan,MPI_Exscan/IexscanMPI_Reduce_scatter/Ireduce_scatter,MPI_Reduce_scatter_block/Ireduce_scatter_blockThese functions are not referenced by any per-shape predicate, so they are added directly to the classification containers without a dedicated
IdentifierInfomember. Neighborhood collectives (MPI_Ineighbor_*) and the wait-family beyondWait/Waitall(Waitany/Waitsome/Test*) remain out of scope.Adds regression tests covering the reported repro (no warning), and confirms double-nonblocking and missing-wait are still detected for the new functions.
Fixes #208107