Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions cuda_core/.clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

# Layout is clang-format's job (see .clang-format), so clang-tidy does no
# formatting of its own (FormatStyle: none below). Each enabled check is meant
# to be adoptable on its own, so findings can be fixed one check at a time.
#
# Checks are opt-in (-*) by group, minus the ones below. They cannot be
# commented inline because Checks is a single YAML string.
#
# Disabled because they conflict with the C-style CUDA driver API idioms:
# bugprone-easily-swappable-parameters: wrappers of driver calls take many
# adjacent parameters of the same type; the signatures mirror the API.
# bugprone-assignment-in-if-condition: `if (CUDA_SUCCESS != (err = CALL(...)))`
# is the standard error-handling pattern here.
# performance-no-int-to-ptr: opaque handles and file descriptors are
# converted to pointers with reinterpret_cast on purpose.
# modernize-avoid-c-arrays: arrays are passed to and filled by C APIs.
# modernize-return-braced-init-list: `return {};` vs. an explicit type is
# left to the author; it adds noise for the driver-API result structs.
# bugprone-throwing-static-initialization: the handle registries are
# process-lifetime globals whose constructors allocate on some standard
# libraries (MSVC); running out of memory while loading the module is
# fatal either way.
#
# Disabled because they do not match the codebase style:
# performance-enum-size: enum underlying types are left as they are.
# misc-const-correctness: const on every local is not the convention here.
# readability-function-cognitive-complexity: too disruptive.
# readability-identifier-length: short names such as the handle parameter `h`
# are used throughout.
# readability-implicit-bool-conversion: `if (ptr)` and `if (handle)` are the
# idiom for pointers and handles.
# misc-include-cleaner: too noisy with the CUDA and CPython umbrella headers.
# misc-no-recursion: recursion is used deliberately.
# misc-use-anonymous-namespace: file-local functions use `static`.
# modernize-use-trailing-return-type: the code uses `Type\nname()` layout.
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-assignment-in-if-condition,
-bugprone-throwing-static-initialization,
clang-analyzer-*,
misc-*,
-misc-include-cleaner,
-misc-const-correctness,
-misc-no-recursion,
-misc-use-anonymous-namespace,
modernize-*,
-modernize-use-trailing-return-type,
-modernize-avoid-c-arrays,
-modernize-return-braced-init-list,
performance-*,
-performance-enum-size,
-performance-no-int-to-ptr,
readability-*,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-readability-implicit-bool-conversion
FormatStyle: none
CheckOptions:
# Records whose members are all public (the *Box and guard structs) are plain
# data holders; only a mix of public and private members is flagged.
misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic: true
WarningsAsErrors: '*'
# Only report diagnostics for our own headers, not CUDA/CPython/libstdc++.
HeaderFilterRegex: 'cuda[/\\]core[/\\]_(cpp|include)[/\\]'
ExcludeHeaderFilterRegex: 'cuda[/\\]core[/\\]_include[/\\](dlpack|aoti_shim)\.h'
1 change: 1 addition & 0 deletions cuda_core/MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ recursive-include cuda/core *.pyx *.pxd *.pxi *.pyi
recursive-include cuda/core/_cpp *.cpp *.h *.hpp
recursive-include cuda/core/_include *.h *.hpp *.def
include build_hooks.py
include .clang-tidy
include cuda/core/py.typed
include NOTICE
# package-local shared PEP 517 helper
Expand Down
163 changes: 163 additions & 0 deletions cuda_core/build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import importlib.util
import os
import re
import subprocess
import sys
import sysconfig
import tempfile
import zipfile
from collections.abc import Sequence
from pathlib import Path

import Cython as _Cython
Expand Down Expand Up @@ -704,6 +707,166 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
return wheel_name


@functools.cache
def _find_clang_tidy() -> str | None:
"""The clang-tidy executable of the build environment.

Isolated builds install the clang-tidy wheel that pyproject.toml requires.
It is located through the wheel's ``clang_tidy`` package. The pixi build
uses the ``clang-tools`` conda package, which puts the executable in a
different location.

PATH is never searched, so a system clang-tidy cannot be picked up.
Returns None when neither is installed.
"""
exe = f"clang-tidy{sysconfig.get_config_var('EXE')}"
if sys.platform == "win32":
conda_dir = Path("Library") / "bin"
else:
conda_dir = "bin"
candidates = [Path(sys.prefix) / conda_dir / exe]
spec = importlib.util.find_spec("clang_tidy")
if spec is not None and spec.submodule_search_locations is not None:
wheel_candidates = [Path(base) / "data" / "bin" / exe for base in spec.submodule_search_locations]
candidates = wheel_candidates + candidates
for candidate in candidates:
if candidate.is_file():
return str(candidate)
return None


@functools.cache
def _warn_clang_tidy_missing() -> None:
"""Warn, once per build, that the clang-tidy checks do not run."""
banner = "!" * 78
print(
f"\n{banner}\n"
"WARNING: clang-tidy is not installed in the build environment, so the clang-tidy\n"
"checks of the handwritten C++ sources will NOT run.\n"
"Isolated builds install the clang-tidy wheel automatically. For other builds,\n"
"install the version that pyproject.toml pins, as the wheel or as the conda-forge\n"
"clang-tools package. The wheel does not exist on Windows ARM64.\n"
f"{banner}\n",
file=sys.stderr,
flush=True,
)


def _partition_handwritten_cpp_files(args: Sequence[str]) -> tuple[list[Path], list[str]]:
"""Split compiler arguments into the handwritten C++ sources and the other arguments.

Cython-generated sources are not handwritten, so they stay among the other
arguments.
"""
handwritten_dir = (_PACKAGE_DIR / "_cpp").resolve()
sources: list[Path] = []
other_args: list[str] = []
for arg in args:
path = Path(arg)
if path.suffix == ".cpp" and path.resolve().is_relative_to(handwritten_dir):
sources.append(path)
else:
other_args.append(arg)
return sources, other_args


def _split_compile_command(compile_command: Sequence[str]) -> tuple[tuple[str, ...], list[str]]:
"""Split a compiler command line into the compiler and its arguments.

The compiler is the leading words that are not options: a compiler launcher
such as sccache or ccache, if any, and the compiler. MSVC options start with
"/", like paths, so there the compiler is the first word only.
"""
n = 1
if sys.platform != "win32":
while n < len(compile_command) and not compile_command[n].startswith("-"):
n += 1
return tuple(compile_command[:n]), list(compile_command[n:])


@functools.cache
def _get_implicit_include_dirs(compiler: tuple[str, ...]) -> list[str]:
"""The include directories that a GCC-style compiler searches without being told.

They are built into the compiler, so they are not on the command line that
setuptools runs, and clang-tidy cannot find them for a toolchain such as the
conda cross-compiler, whose layout it does not know. Examples are the C++
standard library and the compiler's own limits.h. They are returned in
search order. Raises RuntimeError when the compiler does not list them.
"""
# MSVC's include directories come from the INCLUDE environment variable,
# which clang-tidy reads in its MSVC mode. Elsewhere clang-tidy needs the
# include directories that the compiler has built in.
if sys.platform == "win32":
return []

result = subprocess.run( # noqa: S603
[*compiler, "-E", "-x", "c++", "-", "-v"],
input="",
capture_output=True,
text=True,
)
dirs: list[str] = []
in_list = False
for line in result.stderr.splitlines():
if line.startswith("#include <...> search starts here:"):
in_list = True
elif line.startswith("End of search list."):
break
elif in_list and not line.endswith("(framework directory)"):
dirs.append(line.strip())
if result.returncode != 0 or not dirs:
raise RuntimeError(
f"Failed to query the implicit include directories of {' '.join(compiler)}:\n{result.stderr}"
)
return dirs


class ClangTidyError(RuntimeError):
"""clang-tidy reported errors in a handwritten source."""


def check_with_clang_tidy(compile_command: Sequence[str]) -> None:
"""Run clang-tidy on the handwritten .cpp that a compiler command line compiles.

``compile_command`` is the exact command that setuptools is about to spawn,
so clang-tidy sees the same compiler, include directories, macros and
flags as the build. Commands that do not compile a handwritten source are
ignored. The configuration is cuda_core/.clang-tidy, which turns every
finding into an error. Raises ClangTidyError with the findings.

When clang-tidy is not installed, the check is skipped with a prominent
warning, whether or not the command compiles a handwritten source.
"""
clang_tidy = _find_clang_tidy()
if clang_tidy is None:
_warn_clang_tidy_missing()
return
compiler, args = _split_compile_command(compile_command)
# MSVC passes each C++ source as /Tp<path>.
args = [arg.removeprefix("/Tp") for arg in args]
sources, args = _partition_handwritten_cpp_files(args)
if not sources:
return
(source,) = sources
config = Path(__file__).resolve().parent / ".clang-tidy"
if sys.platform == "win32":
# Without this clang-tidy reads the MSVC options, such as /std:c++17,
# as file names. It does not use some of them, such as /c and /GL, and
# reports each unused option as an error.
args.insert(0, "--driver-mode=cl")
args.append("-Wno-unused-command-line-argument")
for include_dir in _get_implicit_include_dirs(compiler):
args.extend(["-isystem", include_dir])
result = subprocess.run( # noqa: S603
[clang_tidy, f"--config-file={config}", "--quiet", source, "--", *args],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise ClangTidyError(f"clang-tidy reported errors in {source}:\n{result.stdout}{result.stderr}")


def _get_cuda_bindings_require():
"""The cuda-bindings build requirement for isolated builds.

Expand Down
4 changes: 2 additions & 2 deletions cuda_core/cuda/core/_cpp/rt/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,15 @@ struct GreenCtxBox {
CUgreenCtx resource;
};

static const ContextBox *
const ContextBox *
get_box(const ContextHandle &h) noexcept
{
const CUcontext *p = h.get();
return reinterpret_cast<const ContextBox *>(reinterpret_cast<const char *>(p) - offsetof(ContextBox, resource));
}

// See REGISTRY_DESIGN.md (Level 1: Driver Handle -> Resource Handle)
static HandleRegistry<CUcontext, ContextHandle> context_registry;
HandleRegistry<CUcontext, ContextHandle> context_registry;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having 'static' or removing it has not effected on the generated binary. However I would like the source code to consistently include static or consistently not include them. Is there a clang-tidy rule or equivalent that we can enable to ensure that is the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason the static is removed here is because it's inside an anonymous namespace (where everything always has internal linkage so that static is redundant). This is enforced by the readability-static-definition-in-anonymous-namespace. We could exclude that rule, which would make static allowed here, but wouldn't enforce that it is always there. My agent (and I) think that no such rule exists.

Outside of anonymous namespaces, static has actual semantic meaning and just needs to be applied correctly, but it wouldn't make sense to always have it or not have it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's basically what I was saying. I don't really care if static is always there or always not there -- as long as we are consistently applying that across the code base.


// Create a context handle reference, with optional green context as source.
ContextHandle
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/_cpp/rt/context_scope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ cleanup_in_context(
}
std::invoke(std::forward<AfterCleanup>(after_cleanup));
if (status != CUDA_SUCCESS || restore != CUDA_SUCCESS) {
char operation_name[160];
char operation_name[kOperationNameSize];
format_operation(operation_name, sizeof(operation_name), name, handle);
if (status != CUDA_SUCCESS) {
report_cuda_error(operation_name, status, detail);
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/_cpp/rt/driver_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace cuda_core::rt {

// The pointers. Null until ensure_fn_table() fills the table. See driver_api.hpp.
#define CUDA_CORE_DEFINE_DRIVER_FN(name, introduced) decltype(&name) p_##name = nullptr;
#define CUDA_CORE_DEFINE_DRIVER_FN(name, introduced) decltype(&(name)) p_##name = nullptr;
CUDA_CORE_DRIVER_FUNCTIONS(CUDA_CORE_DEFINE_DRIVER_FN)
#undef CUDA_CORE_DEFINE_DRIVER_FN

Expand Down
4 changes: 2 additions & 2 deletions cuda_core/cuda/core/_cpp/rt/driver_api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ namespace cuda_core::rt {
X(cuStreamIsCapturing, 10000) \
X(cuThreadExchangeStreamCaptureMode, 10010)

#define CUDA_CORE_DECLARE_DRIVER_FN(name, introduced) extern decltype(&name) p_##name;
#define CUDA_CORE_DECLARE_DRIVER_FN(name, introduced) extern decltype(&(name)) p_##name;
CUDA_CORE_DRIVER_FUNCTIONS(CUDA_CORE_DECLARE_DRIVER_FN)
#undef CUDA_CORE_DECLARE_DRIVER_FN

Expand Down Expand Up @@ -229,7 +229,7 @@ struct Unavailable;
template <class R, class... A>
struct Unavailable<R (*)(A...)> {
static R
call(A...) noexcept
call(A... /*unused*/) noexcept
{
return UnavailableStatus<R>::value;
}
Expand Down
10 changes: 5 additions & 5 deletions cuda_core/cuda/core/_cpp/rt/error.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ namespace {
// Thread-local detail attached to the next raised CUDAError with a matching
// status (see take_last_error_detail()). Written only by propagating helpers.
// The taken copy stays valid until the next take on the same thread.
thread_local char last_error_detail[512] = {0};
thread_local char taken_error_detail[512] = {0};
thread_local char last_error_detail[kErrorMessageSize] = {0};
thread_local char taken_error_detail[kErrorMessageSize] = {0};
thread_local CUresult last_error_detail_status = CUDA_SUCCESS;
} // namespace

Expand Down Expand Up @@ -66,7 +66,7 @@ format_operation(char *buffer, size_t size, const char *operation, unsigned long
void
report_status_code(const char *operation, long code) noexcept
{
char message[256];
char message[kErrorMessageSize];
std::snprintf(message, sizeof(message), "%s failed (status %ld)", operation, code);
report_message(message);
}
Expand All @@ -80,7 +80,7 @@ report_cuda_error(const char *operation, CUresult status, const char *detail) no
if (status == CUDA_SUCCESS || status == CUDA_ERROR_DEINITIALIZED) {
return;
}
char message[512];
char message[kErrorMessageSize];
format_cuda_error(message, sizeof(message), operation, status, detail);
report_message(message);
}
Expand Down Expand Up @@ -122,7 +122,7 @@ note_context_not_restored(CUcontext previous, CUresult operation_status, CUresul
if (DRIVER_CALL(cuCtxGetCurrent, &current) != CUDA_SUCCESS) {
current = nullptr;
}
char cause[128] = {0};
char cause[kErrorMessageSize] = {0};
if (operation_status != CUDA_SUCCESS) {
const char *error_name = nullptr;
if (DRIVER_CALL(cuGetErrorName, restore_status, &error_name) == CUDA_SUCCESS) {
Expand Down
8 changes: 8 additions & 0 deletions cuda_core/cuda/core/_cpp/rt/error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
#pragma once

#include <cuda.h>
#include <cstddef>

namespace cuda_core::rt {

// Sizes of the stack buffers that format error text. Messages are truncated by
// snprintf, never overrun.
constexpr std::size_t kOperationNameSize = 160; // "<call> (handle 0x...)" from format_operation()
constexpr std::size_t kErrorMessageSize = 512; // a full error message or error detail
// A message that embeds one error message after some fixed text.
constexpr std::size_t kNestedMessageSize = kErrorMessageSize + (kErrorMessageSize / 2);

// ============================================================================
// Thread-local error handling
// ============================================================================
Expand Down
Loading
Loading