diff --git a/cccl_upstream/.agent/skills/cccl-style/SKILL.md b/cccl_upstream/.agent/skills/cccl-style/SKILL.md new file mode 100644 index 00000000..6d5b5eb0 --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-style/SKILL.md @@ -0,0 +1,14 @@ +--- +name: cccl-style +description: Use when editing or reviewing CCCL code for style conventions; read common CCCL guidance and the path-specific references named by this skill. +--- + +# CCCL Style + +## Workflow + +1. Read `references/common.md` for guidance that applies across CCCL. +2. For `libcudacxx/include/**/*`, also read `references/libcudacxx.md`. +3. For `cudax/include/**/*`, also read `references/libcudacxx.md`. +4. If no path-specific reference exists, follow nearby code and repository docs. Do not import rules from another subproject. +5. Apply each reference only to its stated scope. Rules for one CCCL subproject do not automatically apply to another. diff --git a/cccl_upstream/.agent/skills/cccl-style/references/common.md b/cccl_upstream/.agent/skills/cccl-style/references/common.md new file mode 100644 index 00000000..e786db1a --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-style/references/common.md @@ -0,0 +1,70 @@ +# Common CCCL Style Guidance + +Apply this guidance across CCCL unless a path-specific style reference says otherwise. + +## Naming Style + +- Macros: macro style, e.g. `MY_MACRO`. +- Template parameters: PascalCase, e.g. `MyParameter`. +- All other symbols: snake style, e.g. `my_variable`. The one exception is the CUB public API, which uses PascalCase. + +## Variables + +- All variables that are not modified must use `const`. This includes variables initialized by casts (`static_cast`, `reinterpret_cast`, `bit_cast`), function return values, and loop-invariant computations. +- All variables that can be evaluated at compile-time must use `constexpr`. +- All `constexpr` variables at namespace/global scope must use `inline`, including variable templates. +- Consider using plural names for array, span, list, e.g. `int values[4]` instead of `int value[4]`. +- Use uniform initialization for class constructors (not enforced to builtin types) and compile-time conversions, e.g. `constexpr auto x = int{sizeof(float)};`. + +## Headers + +- Files must include all headers related to the symbols that they are using. +- Relying on transitive header inclusion is not allowed. +- Unneeded headers must be removed. +- All headers must have the correct license. This also applies to source files. +- All header inclusions must use the syntax `
`. +- Use forward declaration, namely `__fwd/header.h` or direct type declaration, when possible instead of including the implementation header. +- Headers should be the most precise available, e.g. `#include `. +- Do not include headers in `cuda/std/__cccl/` directly; they are provided by `__config` or the prologue/epilogue mechanism. + +## Functions + +- Functions must be marked `_CCCL_HOST_API`, `_CCCL_DEVICE_API`, `_CCCL_HOST_DEVICE_API`, `_CCCL_TILE_API`, or `_CCCL_API`. +- Non-template, non-`constexpr` functions must use `inline`. +- Most functions with a non-void return type should use `[[nodiscard]]`; functions with known side effects may be exceptions. +- Functions that do not throw exceptions must use `noexcept`. +- Use `_CCCL_CONSTEVAL` when the function can only be evaluated at compile time. +- Use C++20 concept macros instead of SFINAE, e.g. `_CCCL_TEMPLATE(...)` and `_CCCL_REQUIRES(...)`. + +## Function Calls And Types + +- In headers, apply global qualification where the subproject requires it: + - libcudacxx and cudax require free function calls to be fully qualified from the global namespace, e.g. `::cuda::ceil_div(...)`. + - CUB applies this rule only to calls to symbols under the `::cuda` namespace hierarchy; otherwise follow existing CUB qualification style. + - Thrust uses leading `::` for many symbols under the `::cuda` namespace hierarchy, but relies on ADL in many places and the blanket free-function qualification rule does not apply to those calls. +- For covered calls, this includes calls to functions defined in the same namespace, e.g. inside `cuda::`, call `::cuda::ceil_div(...)`, not `ceil_div(...)`. This does not apply to (static) member functions of classes. The only exceptions for covered calls are functions that are supposed to be found through argument-dependent lookup (ADL), such as `::cuda::std::swap` and `::cuda::std::get`. Those functions can be called unqualified with a preceding `using ::cuda::std::get;`. +- This global-qualification rule does not apply to source files such as tests and benchmarks. +- In headers, apply type-name qualification where the subproject requires it: + - libcudacxx and cudax require type names to be fully qualified except when they are already declared in the current namespace or an enclosing one. Outside those namespaces, fully qualify `cuda::std` and standard integer type aliases such as `::cuda::std::size_t`. + - CUB applies this rule only to type names under the `::cuda` namespace hierarchy. Do not apply the libcudacxx/cudax blanket type-qualification rule to CUB namespaces or `detail` namespaces. + - Thrust does not apply the libcudacxx/cudax blanket type-qualification rule. It uses leading `::` for many `::cuda` and `::cuda::std` type names, but also uses Thrust namespace patterns; follow neighboring Thrust code. +- A local `using` declaration, e.g. `using ::cuda::std::size_t;`, is acceptable to avoid repetition within a function body. +- Static member functions of a class template inherit the class's namespace. + +## Comments + +- Commented code without a description is not allowed. + +## General Guidelines + +- The code must reuse `cuda/` or `cuda/std` functionalities as much as possible, including macros. +- Try to use modern C++ as much as possible. The repository supports C++17 but many more recent functionalities have been backported with functions and macros. + +## Prevent Compiler Errors And Improve Compatibility + +- Remove unused code, variables, functions, types, template parameters, headers, etc. +- Variables that are unsigned, or that can become unsigned after template instantiation, must not check for negative values directly. Use `cuda::std::is_unsigned_v ? false : (var < 0)` instead. + +## Compiler Compatibility + +- Protect host-only code with `#if !_CCCL_COMPILER(NVRTC)`. diff --git a/cccl_upstream/.agent/skills/cccl-style/references/libcudacxx.md b/cccl_upstream/.agent/skills/cccl-style/references/libcudacxx.md new file mode 100644 index 00000000..c3295634 --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-style/references/libcudacxx.md @@ -0,0 +1,55 @@ +# libcudacxx Style Guidance + +Use this reference for `libcudacxx/include/**/*` and `cudax/include/**/*`. + +## Naming Style + +All non-public symbols must be C++ reserved identifiers: + +- `_` for macros and template parameters, e.g. `_MY_MACRO`, `_MyParameter`. +- `__` for all other symbols, e.g. `__my_variable`. +- Never use reserved keywords, such as `__in`, `__out`, or `__inout` as variables, parameters, or function names. +- Avoid single-letter template parameter names. Wrong: `_T`; correct: `_Tp`. + +## Class / Struct + +- Data member names have postfix `_`, e.g. `class __myclass { int __data_; };`. +- Constructor parameter names should match class/struct data member names without the postfix `_`, e.g. `class __myclass { __myclass(int __data) : __data_(__data) {} };`. + +## Functions + +- Use `constexpr` for functions that do not depend on run-time features, such as pointers. +- If the return type is not explicit (`auto`), then a trailing return type is strongly preferred. + +## Headers + +- Use the correct license: + - `libcudacxx/include/cuda/std` files ported from LLVM libc++ use the LLVM license. + - `libcudacxx/include/cuda/` files use Apache License v2.0 with LLVM Exceptions. +- Headers use include guards with names derived from the uppercase full path and closing `#endif` comments repeating the guard name. +- Right after the include guard, include: + +```cpp +#include + +#if defined(_CCCL_IMPLICIT_SYSTEM_HEADER_GCC) +# pragma GCC system_header +#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_CLANG) +# pragma clang system_header +#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_MSVC) +# pragma system_header +#endif // no system header +``` + +- The last included header before code must be ``, and `` must appear at the end of the file. + +## Comments + +- Use Doxygen-style `//! @brief` comments. +- Documented functions must include `//! @brief`, `//! @param[in/out/in,out]` for every parameter, and `//! @return` for non-void functions. +- The `@brief/@param/@return` description must accurately reflect the current functionality of the function. + +## Compiler Compatibility + +- Do not use lambda expressions in device-only or host-device code. +- Do not rely on deduction guides for initialization; use explicit template arguments instead. diff --git a/cccl_upstream/.agent/skills/cccl-test/SKILL.md b/cccl_upstream/.agent/skills/cccl-test/SKILL.md new file mode 100644 index 00000000..8fb2f2b6 --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-test/SKILL.md @@ -0,0 +1,13 @@ +--- +name: cccl-test +description: Use when writing, updating, reviewing, or validating CCCL tests; read common CCCL test guidance and the path-specific references named by this skill. +--- + +# CCCL Test + +## Workflow + +1. Read `references/common.md` for guidance that applies across CCCL tests. +2. For `libcudacxx/test/**/*`, also read `references/libcudacxx.md`. +3. If no path-specific reference exists, follow nearby tests and repository docs. Do not import test rules from another subproject. +4. Apply each reference only to its stated scope. Rules for one CCCL subproject do not automatically apply to another. diff --git a/cccl_upstream/.agent/skills/cccl-test/references/common.md b/cccl_upstream/.agent/skills/cccl-test/references/common.md new file mode 100644 index 00000000..01f20983 --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-test/references/common.md @@ -0,0 +1,31 @@ +# Common CCCL Test Guidance + +Apply this guidance across CCCL tests unless a path-specific test reference says otherwise. + +## Local Consistency + +- Read nearby tests first and mirror their directory layout, file names, helper types, includes, assertion style, and local gating or skip mechanisms. + +## Coverage + +- Cover relevant edge cases. +- Cover relevant input and output types. +- Cover error behavior when applicable. +- Cover runtime and compile-time behavior when applicable. +- Cover device and host behavior when applicable. + +## Test Structure + +- All tests must have the correct license banner. +- Use the local test harness assertions and helpers. +- Use compile-time checks for compile-time guarantees and constexpr coverage when relevant. +- Negative tests should check the intended diagnostic or failure mode when the local harness supports it. + +## Portability + +- Prefer project test macros and helpers for compiler, dialect, exception, host/device, and platform probes instead of spelling ad hoc checks directly. +- If a test is unsupported, expected to fail, disabled, or skipped on a platform, motivate it with a comment. + +## Validation + +- Use targeted test runs for the project and files being changed. diff --git a/cccl_upstream/.agent/skills/cccl-test/references/libcudacxx.md b/cccl_upstream/.agent/skills/cccl-test/references/libcudacxx.md new file mode 100644 index 00000000..14921f10 --- /dev/null +++ b/cccl_upstream/.agent/skills/cccl-test/references/libcudacxx.md @@ -0,0 +1,83 @@ +# libcudacxx Test + +## Organization + +- Put CUDA Standard Library tests under `libcudacxx/test/libcudacxx/std/...`. +- Put CUDA-specific API tests under `libcudacxx/test/libcudacxx/cuda/...`, unless an adjacent `std/...` directory is clearly the established home for the functionality. +- Read nearby tests first and mirror their directory layout, file names, helper types, includes, and lit gates. + +## Purpose + +- Validate libcudacxx functionality. It is fundamental to verify: + + - Edge cases. + - Input and output types. + - Exception behavior. + - Runtime and constant-evaluation behavior. + - Device and host behavior. + +## Test kinds + +- `.pass.cpp`: compiles, links, runs, and returns 0. +- `.compile.pass.cpp`: compiles correctly. +- `.fail.cpp`: must fail compilation. Prefer precise `expected-error`, `expected-warning`, `expected-note`, or `expected-no-diagnostics` annotations when clang verify is supported. +- `.runfail.cpp`: compiles and runs but must return non-zero. + +## Test structure + +- All tests must have the correct license banner. +- Always include top level headers, never internal ones with `__` prefix. +- Include support headers `"test_macros.h"`, `"test_iterators.h"`, `"test_comparisons.h"`, when needed. +- Use `static_assert(...)` for compile-time guarantees and constexpr coverage. +- Use `` and `assert(...)` for runtime checks. +- The `main` function must be present, dispatch runtime and static-evaluation tests, and return 0. + +## Style + +- Use `cuda::std` names, not `std::` names, unless the test is intentionally checking interoperability with host standard library types. +- Do not fully qualify names in header includes unless the test is intentionally checking interoperability with host standard library types. +- Mark helper functions that may run on host and device with `TEST_FUNC`; use `TEST_DEVICE_FUNC` for device-only helpers. + use `TEST_TILE_FUNC` for tile only helpers and `TEST_TILE_DEVICE_FUNC` for functions that can run on tile and device +- `const`-qualification is discouraged. +- Don't use `noexcept` for helper functions unless strictly necessary. +- Do not use lambda expressions in host/device test code unless nearby tests already prove the pattern is supported. + +## Portability + +- Guard host-only or device-only behavior with `NV_IF_TARGET(NV_IS_HOST, (...))` and `NV_IF_TARGET(NV_IS_DEVICE, (...))` respectively. +- Use `TEST_STD_VER`, `TEST_COMPILER`, `TEST_CUDA_COMPILER`, `TEST_HAS_EXCEPTIONS`, and `TEST_THROW` from `"test_macros.h"` instead of spelling compiler or dialect probes directly. +- Unsupported platforms can be disabled with `UNSUPPORTED: ` or `XFAIL: ` lit directives. Some common feature names are `nvrtc`, `enable-tile`, `pre-sm-70`, `c++17`, `c++20`, `msvc`, `gcc-`, or `clang-`. + + - Always motivate unsupported features with a comment. + +## Lit directives + +- Put lit directives near the top of the file before includes. +- Common directives: `UNSUPPORTED:`, `XFAIL:`, `REQUIRES:`, `ADDITIONAL_COMPILE_DEFINITIONS:`, `ADDITIONAL_COMPILE_OPTIONS_HOST:`, `ADDITIONAL_COMPILE_OPTIONS_CUDA:`, `MODULES_DEFINES:`, and `CONSTEXPR_STEPS:`. +- For diagnostics in `.fail.cpp`, annotate the exact line that should fail when possible: + +```cpp +bad_expression(); // expected-error {{message fragment}} +``` + +- Prefer checking the intended diagnostic over accepting any compile failure. + +## Validation + +Use targeted libcudacxx lit runs. Paths passed to `--lit-precompile-tests` and `--lit-tests` are relative to `libcudacxx/test/libcudacxx/`. + +```bash +ci/util/build_and_test_targets.sh \ + --preset libcudacxx \ + --lit-precompile-tests "std/algorithms/alg.nonmodifying/alg.any_of/any_of.pass.cpp" \ + --lit-tests "std/algorithms/alg.nonmodifying/alg.any_of/any_of.pass.cpp" +``` + +If running lit directly, use the configured site file: + +```bash +LIBCUDACXX_SITE_CONFIG=/build//libcudacxx/test/libcudacxx/lit.site.cfg \ + lit -v libcudacxx/test/libcudacxx/ +``` + +- Use `-Dexecutor=NoopExecutor()` for precompile-only validation when runtime execution is unavailable or GPU coverage is not required. diff --git a/cccl_upstream/.agent/skills/sass-diff/SKILL.md b/cccl_upstream/.agent/skills/sass-diff/SKILL.md new file mode 100644 index 00000000..4dd594e2 --- /dev/null +++ b/cccl_upstream/.agent/skills/sass-diff/SKILL.md @@ -0,0 +1,50 @@ +--- +name: sass-diff +description: Use when asked to check for SASS (or PTX) changes between commits, branches, or a local changeset; guides normalization, comparison, and reporting of CUDA disassembly diffs. +--- + +# SASS Diffs + +Use this when asked to check for SASS changes between commits, branches or a local changeset. + +## Goal + +Detect relevant changes in generated CUDA machine code (i.e. SASS) while filtering noise from addresses, symbols, metadata, etc. +Any non-trivial change must be detected. + +## Inputs to establish + +* Compilation target under test +* The CUDA SM architectures to compile for. Try to detect this from the code and offer the user a list of suggestions. + The user must confirm or provide this list. +* Baseline source (e.g. the previous commit/branch or the current commit without the changes in the working copy). +* Comparison source (e.g. the current commit/branch or the current commit with the changes in the working copy). +* Whether a SASS (default) or PTX diff is requested. + +## Disassembly listing generation + +* Compile both, the baseline and comparison source, with the same compiler flags and options. + When not specified otherwise, lookup the options from `compile_commands.json` + or the current build system (i.e. CMake files). + Make sure the CUDA SM architectures (`CMAKE_CUDA_ARCHITECTURES`) are set to the user-provided or approved list. +* Dump the disassembly from the binaries produced in the previous set using `cuobjdump -sass` or `cuobjdump -ptx`. + +## Comparison rules (what matters) + +Ignore as trivial: + +* Register renaming with identical instruction sequence and operands. +* Pure label renumbering or reordering of identical basic blocks. +* Formatting-only differences or reordered symbol tables. +* Changes to symbol names (global function names) + +## Reporting + +* If any non-trivial change was detected, report the top 5 regions where a non-trivial change was detected, + including the name of the kernel they appeared in. +* Provide a short summary of the diff type, + including opcode changes, memory access size/cache policy changes, control-flow changes, register-count changes, + spills/local memory, shared memory, and occupancy-relevant resource deltas. +* Explicitly state if only noise was detected. +* If you are not sure if the differences are impactful, show it and ask the user for guidance. +* Keep the disassembly dumps available and tell the user where they can find them. diff --git a/cccl_upstream/.clang-format b/cccl_upstream/.clang-format new file mode 100644 index 00000000..bfcbb33c --- /dev/null +++ b/cccl_upstream/.clang-format @@ -0,0 +1,205 @@ +# Note that we don't specify the language in this file because some files are +# detected as Cpp, but others are detected as ObjC and we want this formatting +# to apply to all types of files. +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: Consecutive +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveMacros: Consecutive +AlignEscapedNewlines: Left +AlignOperands: AlignAfterOperator +AllowAllArgumentsOnNextLine: true +AlignTrailingComments: + Kind: Never +AllowAllParametersOfDeclarationOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: [ + '_CCCL_ALIGNAS_TYPE', + '_CCCL_ALIGNAS', + '_CCCL_CONSTEXPR_CXX20', + '_CCCL_CONSTEXPR_CXX23', + '_CCCL_DECLSPEC_EMPTY_BASES', + '_CCCL_DEVICE', + '_CCCL_FORCEINLINE', + '_CCCL_HIDE_FROM_ABI', + '_CCCL_HOST_DEVICE', + '_CCCL_DEDUCTION_GUIDE_ATTRIBUTES', + '_CCCL_HOST', + '_CCCL_KERNEL_ATTRIBUTES', + '_CCCL_NO_UNIQUE_ADDRESS', + '_CCCL_TYPE_VISIBILITY_DEFAULT', + '_CCCL_TYPE_VISIBILITY_HIDDEN', + '_CCCL_VISIBILITY_HIDDEN', + '_CCCL_LAUNCH_BOUNDS', + '_CCCL_BLOCK_SIZE', + 'CUB_RUNTIME_FUNCTION', + 'THRUST_RUNTIME_FUNCTION', + 'CCCL_DEPRECATED', + 'CCCL_DEPRECATED_BECAUSE', + '_CCCL_DEPRECATED_IN_CXX20', + '_CCCL_DEPRECATED_IN_CXX23', + '_CCCL_API', + '_CCCL_HOST_API', + '_CCCL_DEVICE_API', + '_CCCL_NODEBUG_API', + '_CCCL_NODEBUG_HOST_API', + '_CCCL_NODEBUG_DEVICE_API', + '_CCCL_TRIVIAL_API', + '_CCCL_TRIVIAL_HOST_API', + '_CCCL_TRIVIAL_DEVICE_API', + '_CCCL_PUBLIC_API', + '_CCCL_PUBLIC_HOST_API', + '_CCCL_PUBLIC_DEVICE_API', + ] +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false +BreakBeforeConceptDeclarations: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +ColumnLimit: 120 +CompactNamespaces: false +ContinuationIndentWidth: 2 +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: true +IfMacros: [ + '_CUB_WEAKEN_IF_CONSTEXPR_IF_COMPILED_FOR_CCCL_C', + '_CCCL_CATCH' +] +IndentWrappedFunctionNames: false +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 0x7FFFFFFF + SortPriority: 0x7FFFFFFF + - Regex: '^' + Priority: -0x7FFFFFFF + SortPriority: -0x7FFFFFFF + - Regex: '^' + Priority: 0x7FFFFFFF + SortPriority: 0x7FFFFFFF + - Regex: '^"insert_nested_NVTX_range_guard.h"' + Priority: -1 + SortPriority: -1 + - Regex: '^<(cuda/std/detail/__config|cub/config.cuh|thrust/detail/config.h|thrust/system/cuda/config.h)' + Priority: 0 + SortPriority: 0 + - Regex: '^$' + Priority: 7 + SortPriority: 6 + - Regex: '^<[a-z_]*\.[a-z]+>$' + Priority: 8 + SortPriority: 7 + - Regex: '^ +- _CCCL_REQUIRES(...)=requires (...) +- _CUDAX_SEMI_PRIVATE=private +NamespaceIndentation: None +PackConstructorInitializers: Never +PenaltyBreakAssignment: 30 +PenaltyBreakBeforeFirstCallParameter: 50 +PenaltyBreakComment: 0 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 70 +PenaltyBreakTemplateDeclaration: 0 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 90 +PenaltyIndentedWhitespace: 2 +PointerAlignment: Left +ReflowComments: true +RemoveSemicolon: false +SortIncludes: CaseInsensitive +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +StatementMacros: [ + '_CCCL_EXEC_CHECK_DISABLE', + 'CUB_NAMESPACE_BEGIN', + 'CUB_NAMESPACE_END', + 'THRUST_NAMESPACE_BEGIN', + 'THRUST_NAMESPACE_END', + '_CCCL_BEGIN_NAMESPACE_CUDA_STD', + '_CCCL_END_NAMESPACE_CUDA_STD', + '_CCCL_BEGIN_NAMESPACE_CUDA_STD_NOVERSION', + '_CCCL_END_NAMESPACE_CUDA_STD_NOVERSION', + '_CCCL_BEGIN_NAMESPACE_CUDA_STD_RANGES', + '_CCCL_END_NAMESPACE_CUDA_STD_RANGES', + '_CCCL_BEGIN_NAMESPACE_CUDA_STD_RANGES_ABI', + '_CCCL_END_NAMESPACE_CUDA_STD_RANGES_ABI', + '_CCCL_BEGIN_NAMESPACE_CUDA_STD_VIEWS', + '_CCCL_END_NAMESPACE_CUDA_STD_VIEWS', + '_CCCL_BEGIN_NAMESPACE_CPO', + '_CCCL_END_NAMESPACE_CPO', +] +TabWidth: 2 +UseTab: Never +WrapNamespaceBodyWithEmptyLines: Never diff --git a/cccl_upstream/.clang-tidy b/cccl_upstream/.clang-tidy new file mode 100644 index 00000000..ea9e95c4 --- /dev/null +++ b/cccl_upstream/.clang-tidy @@ -0,0 +1,324 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +Checks: + # enable everything first + - 'performance-*' + - 'modernize-*' + - 'readability-*' + - 'clang-analyzer-*' + - 'clang-diagnostic-*' + - 'bugprone-*' + - 'misc-*' + - 'core-*' + - 'mpi-*' + - 'cert-*' + - 'portability-*' + - 'google-*' + - 'cppcoreguidelines-*' + - 'concurrency-*' + # TODO: BEGIN REMOVE ME + - '-*' + - 'performance-*' + - '-performance-enum-size' + - '-performance-unnecessary-value-param' + - 'modernize-*' + - '-modernize-use-integer-sign-comparison' + - '-modernize-use-nodiscard' + - '-modernize-return-braced-init-list' + - '-modernize-use-auto' + - 'clang-diagnostic-*' + - 'bugprone-*' + - '-bugprone-crtp-constructor-accessibility' + - '-bugprone-argument-comment' + # This is just noise + - '-bugprone-signed-char-misuse' + - '-bugprone-multi-level-implicit-pointer-conversion' + # END REMOVE ME + # TODO(jfaibussowit): + # + # Enable these once we move to C++20. Normally, clang-tidy does not suggest checks for + # cpp N+1 if you are compiling with -std=c++N. However some subprojects like c.parallel + # actually require cpp N+1 so clang-tidy emits diagnostics for any headers included by + # it. + - '-modernize-use-designated-initializers' + - '-modernize-use-constraints' + - '-modernize-use-ranges' + # Irrelevant for GPU code + - '-modernize-pass-by-value' + # HICPP is 99% aliased to other checks (mostly modernize-* and bugprone-*). We don't + # want to also enable it because then we need to duplicate the NOLINT. The only 2 checks + # that aren't aliases are: + # + # - hicpp-multiway-paths-covered. This check is useful for detecting degenerate if-else + # branches, but the switch cases are IMO better handled by -Wswitch. To make -Wswitch + # even stronger, we should ban default: cases entirely, + # + # - hicpp-signed-bitwise. This check is also covered by + # clang.llvm.org/extra/clang-tidy/checks/clang-analyzer/core.BitwiseShift.html + # + # - 'hicpp-*' + # + # LLVM checks are extremely specific to the LLVM project and aren't suitable for + # downstream users. + # + # - 'llvm-*' + # + # then disable the stuff we don't want + - '-cert-dcl21-cpp' # returning non-const from operator-- or operator++ + - '-cert-dcl50-cpp' # allow c-style variadics + # No reserved identifiers, both of these are aliased to bugprone-reserved-identifier, + # which we do enable. Leaving these enabled however, leads to needing to specify all + # three (bugprone-reserved-identifier, cert-dcl51-cpp, and cert-dcl37-c) in NOLINT lines + # which is a hassle. Since bugprone-reserved-identifier is enabled, the check still + # fires. + - '-cert-dcl51-cpp,-cert-dcl37-c,-cert-oop54-cpp' + # Covered by bugprone-throwing-static-initialization + - '-cert-err58-cpp' + - '-modernize-use-trailing-return-type' + - '-readability-function-cognitive-complexity' + - '-readability-implicit-bool-conversion' + - '-readability-braces-around-statements' + - '-readability-qualified-auto' + - '-readability-isolate-declaration' + - '-modernize-avoid-c-arrays' + - '-cppcoreguidelines-avoid-c-arrays' + - '-readability-named-parameter' + - '-readability-identifier-length' + - '-misc-non-private-member-variables-in-classes' + - '-bugprone-easily-swappable-parameters' + - '-bugprone-implicit-widening-of-multiplication-result' + - '-misc-include-cleaner' + - '-misc-header-include-cycle' + - '-modernize-macro-to-enum' + - '-cppcoreguidelines-macro-to-enum' + - '-misc-no-recursion' + - '-bugprone-dynamic-static-initializers' + - '-portability-avoid-pragma-once' + # Generally speaking if something is static then it has an express reason to be. 99% of + # the candidates identified by this check were because they just so happened not to + # touch any member variables, not because they are logically static. So we disable the + # check. + - '-readability-convert-member-functions-to-static' + # When running in a conda env, ignore options that may have been added by conda but are unused + - '-clang-diagnostic-unused-command-line-argument' + # Given + # + # std::make_pair(...) + # + # Results in: error: for C++11-compatibility, use pair directly. But we don't care about + # C++11, and so we don't care about this warning. + - '-google-build-explicit-make-pair' + # This check is incredibly expensive for absolutely no reason, and since we a) use + # modern google-test and b) don't have clang-tidy enabled on our testing code, we don't + # need to enable it! + - '-google-upgrade-googletest-case' + # This warning will warn if you have redundant default-initializers for class + # members. For example: + # + # class Foo + # { + # int x_{}; // WARNING: redundant initializer here + # + # public: + # Foo(int x) : x_{x} { } + # }; + # + # Since Foo can only ever be constructed with an explicit value for x_ via its constructor, + # the default initializer is technically redundant. However, if we change the definition + # of Foo to now allow a default ctor, then the initializer becomes non-redundant + # again. It is easier to just follow the rule of "always explicitly default initialize + # members" than to remember to change 2 places at once. + - '-readability-redundant-member-init' + # Alias for readability-enum-initial-value, disable this one because the readability- + # name is easier to understand, and we don't want to silence 2 things for the same + # warning. + - '-cert-int09-c' + # This one is potentially controversial. This check warns when iterating over unordered + # containers of pointers: + # + # { + # int a = 1, b = 2; + # std::unordered_set set = {&a, &b}; + # + # for (auto *i : set) { // iteration order not deterministic + # f(i); + # } + # } + # + # On the one hand, this is a clear case of non-determinism. But on the other hand, I + # feel it is obvious that a user does not care about the order of iteration + # because... they are using unordered containers! + - '-bugprone-nondeterministic-pointer-iteration-order' + - '-cppcoreguidelines-pro-type-reinterpret-cast' + # We don't use GSL + - '-cppcoreguidelines-owning-memory' + # Covered by modernize-use-override already + - '-cppcoreguidelines-explicit-virtual-functions' + # Covered by readability-magic-numbers + - '-cppcoreguidelines-avoid-magic-numbers' + # This check does more harm than good, as it appears to be very rudimentary. For + # example, it emits warnings saying that: + # + # #define DEFINE_OPERATOR(op) \ + # _CCCL_HOST_DEVICE custom_numeric operator op() const \ + # { \ + # return custom_numeric(op value[0]); \ + # } + # + # Should be replaced by a template function, but this is not possible. We instead rely + # on best judgment of reviewers to catch macros that can be functions. + - '-cppcoreguidelines-macro-usage' + # This check does not understand more complex macro usage where brackets are not allowed + # + # error: macro replacement list should be enclosed in parentheses [bugprone-macro-parentheses] + # 36 | #define __THRUST_HOST_SYSTEM_INCLUDE(filename) <__THRUST_HOST_SYSTEM_ROOT/filename> + # | ^ + # | ( ) + # + # So better left disabled. + - '-bugprone-macro-parentheses' + + # TODO: Enable for clang-tidy-22 + - '-modernize-avoid-c-style-cast' + - '-bugprone-signed-bitwise' + - '-bugprone-std-namespace-modification' + - '-clang-diagnostic-deprecated-attributes' + - '-bugprone-throwing-static-initialization' + - '-bugprone-unchecked-string-to-number-conversion' + - '-bugprone-invalid-enum-default-initialization' + - '-bugprone-derived-method-shadowing-base-method' + - '-bugprone-unsafe-to-allow-exceptions' + - '-bugprone-sizeof-expression' + - '-modernize-use-nullptr' + - '-bugprone-std-exception-baseclass' + - '-modernize-avoid-variadic-functions' + - '-modernize-type-traits' + - '-bugprone-random-generator-seed' + - '-bugprone-command-processor' + + # TODO: ironically enough, enable this someday + - '-google-readability-todo' + # Covered by modernize-avoid-c-style-cast, and also is buggier. It seems to fire on + # `_v`-style constexpr bools as well sometimes. + - '-google-readability-casting' + # Covered by performance-noexcept-move-constructor + - '-cppcoreguidelines-noexcept-move-operations' + # REVIEW ME: This warns about any usage of operator[], suggesting usage of .at() instead. Could + - '-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access' + - '-cppcoreguidelines-narrowing-conversions' + # Covered by misc-unconventional-assign-operator + - '-cppcoreguidelines-c-copy-assignment-signature' + # Covered by performance-noexcept-swap + - '-cppcoreguidelines-noexcept-swap' + # Covered by modernize-use-default-member-init + - '-cppcoreguidelines-use-default-member-init' + # clang-tidy documentation says that for a given file, it matches the closest + # .clang-tidy up the directory stack and applies the configuration from it. However, by + # "it" `clang-tidy` means the closest `.clang-tidy` to the *source* file, not the file + # that was included by the source file. + # + # So `cuda/std/foo.h` included by `cccl/foo/bar/baz.cpp` will never "match" against + # `cccl/libcudacxx/.clang-tidy` because `clang-tidy` finds `cccl/.clang-tidy` instead. + # + # So we disable this for now. In the future we should add a custom check that bans + # reserved identifiers except symbols from libcu++. + - '-bugprone-reserved-identifier' +WarningsAsErrors: '*' +HeaderFileExtensions: + - '' + - h + - hh + - hpp + - hxx + - cuh + - inl + - ipp +ImplementationFileExtensions: + - c + - cc + - cpp + - cxx + - cu +SystemHeaders: false +HeaderFilterRegex: '.*/(thrust|cub|cuda|nv|cccl|c2h|cudastf|test)/.*' +ExtraArgsBefore: + # To match _CCCL_DOXYGEN_INVOKED + - '-D_CCCL_CLANG_TIDY_INVOKED=1' + - '-ftemplate-backtrace-limit=0' + - '-fmacro-backtrace-limit=0' +ExtraArgs: + # These must come in ExtraArgs not ExtraArgsBefore, because they are overriding the + # effects of -Wall and -Werror, where the last flag "wins". + - '-Wno-unknown-warning-option' + - '-Wno-unknown-cuda-version' + - '-Wno-error=unused-command-line-argument' +CheckOptions: + cert-dcl16-c.NewSuffixes: 'L;LL;LU;LLU' + cert-err33-c.CheckedFunctions: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;' + llvm-else-after-return.WarnOnUnfixable: 'false' + cert-str34-c.DiagnoseSignedUnsignedCharComparisons: 'false' + cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic: 'true' + google-readability-braces-around-statements.ShortStatementLines: '1' + llvm-qualified-auto.AddConstToQualified: 'false' + llvm-else-after-return.WarnOnConditionVariables: 'false' + cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField: 'false' + performance-move-const-arg.CheckTriviallyCopyableMove: 'false' + performance-inefficient-string-concatenation.StrictMode: 'true' + readability-simplify-boolean-expr.ChainedConditionalReturn: 'true' + readability-simplify-boolean-expr.ChainedConditionalAssignment: 'true' + bugprone-dangling-handle.HandleClasses: '::cuda::std::span;::cuda::std::mdspan' + bugprone-unused-return-value.AllowCastToVoid: 'true' + readability-enum-initial-value.AllowExplicitZeroFirstInitialValue: 'false' + readability-enum-initial-value.AllowExplicitSequentialInitialValues: 'false' + readability-redundant-access-specifiers.CheckFirstDeclaration: 'true' + bugprone-lambda-function-name.IgnoreMacros: 'true' + # readability-identifier-naming.ClassCase: 'lower_case' + # readability-identifier-naming.UnionCase: 'lower_case' + # readability-identifier-naming.ClassConstantCase: 'UPPER_CASE' + # readability-identifier-naming.ClassIgnoredRegexp: 'tuple|has_.*|is_.*|as_.*|.*_tag|tag|.*_of' + # readability-identifier-naming.ConstantMemberCase: 'UPPER_CASE' + # readability-identifier-naming.ConstantMemberIgnoredRegexp: 'value' + # readability-identifier-naming.EnumCase: 'lower_case' + # readability-identifier-naming.EnumConstantCase: 'UPPER_CASE' + # readability-identifier-naming.FunctionCase: 'lower_case' + # readability-identifier-naming.GlobalConstantCase: 'UPPER_CASE' + # readability-identifier-naming.GlobalConstantIgnoredRegexp: '.*_v' + # readability-identifier-naming.LocalVariableCase: 'lower_case' + # # We want to allow constexpr auto MY_VAL1 + # readability-identifier-naming.LocalVariableIgnoredRegexp: '[A-Z_0-9]+' + # readability-identifier-naming.MacroDefinitionCase: 'UPPER_CASE' + # # We want to allow MY_MACRO_PRIVATE_1_ + # readability-identifier-naming.MacroDefinitionIgnoredRegexp: '[A-Z_0-9]+' + # readability-identifier-naming.NamespaceCase: 'lower_case' + # readability-identifier-naming.PrivateMemberCase: 'lower_case' + # readability-identifier-naming.PrivateMemberSuffix: '_' + # readability-identifier-naming.PrivateMethodCase: 'lower_case' + # readability-identifier-naming.PrivateMethodSuffix: '_' + # readability-identifier-naming.ProtectedMemberCase: 'lower_case' + # readability-identifier-naming.ProtectedMemberSuffix: '_' + # readability-identifier-naming.ProtectedMethodCase: 'lower_case' + # readability-identifier-naming.ProtectedMethodSuffix: '_' + # readability-identifier-naming.PublicMethodCase: 'lower_case' + # readability-identifier-naming.ScopedEnumCase: 'lower_case' + # readability-identifier-naming.ScopedEnumConstantCase: 'UPPER_CASE' + readability-magic-numbers.IgnoredIntegerValues: '0;1;2;3;4;5;6;7;8;9' + # There are some single-argument functions that we would not like to ignore + # (`to_string(bool provenance = false)` comes to mind), but setting this to false makes + # clang-tidy warn on a bunch of "obvious" calls, like `set_dim(1)` or + # `with_concurrent(true)`. + bugprone-argument-comment.IgnoreSingleArgument: true + bugprone-argument-comment.CommentBoolLiterals: true + bugprone-argument-comment.CommentIntegerLiterals: true + bugprone-argument-comment.CommentFloatLiterals: true + bugprone-argument-comment.CommentStringLiterals: true + bugprone-argument-comment.CommentCharacterLiterals: true + bugprone-argument-comment.CommentUserDefinedLiterals: true + bugprone-argument-comment.CommentNullPtrs: true + cppcoreguidelines-macro-usage.AllowedRegexp: '^DEBUG_.*|THRUST_PP_.*|_CCCL.*_PP_.*' + bugprone-reserved-identifier.AllowedIdentifiers: '^_CCCL.*' + performance-enum-size.EnumIgnoreList: '::cuda::std::.*' + performance-unnecessary-value-param.AllowedTypes: '.*(_ref(erence)?|_ptr|pointer|(_)?[Ii]t(erator)?|IteratorT|__half(2)?|(__nv_bfloat.*)|_view)$' + bugprone-exception-escape.CheckMain: false +... diff --git a/cccl_upstream/.pre-commit-config.yaml b/cccl_upstream/.pre-commit-config.yaml new file mode 100644 index 00000000..eb6c68b2 --- /dev/null +++ b/cccl_upstream/.pre-commit-config.yaml @@ -0,0 +1,143 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +ci: + autofix_commit_msg: | + [pre-commit.ci] auto code formatting + autofix_prs: false + autoupdate_branch: '' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: quarterly + skip: [mypy, secret-scan-trufflehog] # mypy fails on pre-commit.ci due to numpy stub incompatibility with Python 3.14 + submodules: false + +repos: + # Runs first so a leaked credential blocks the commit before any formatter runs. + # Self-installing: the hook downloads a pinned, checksum-verified trufflehog on + # first use (no manual install). Skipped on pre-commit.ci; Pulse CI enforces server-side. + - repo: https://github.com/NVIDIA/security-workflows + rev: v0.2.0 + hooks: + - id: secret-scan-trufflehog + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + # handled by clang-format + exclude_types: [c, c++, cuda] + - id: check-json + - id: check-toml + - id: pretty-format-json + args: ['--autofix', '--indent=2', '--no-sort-keys'] + - id: check-symlinks + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + # ruff checks this already + exclude_types: [python] + - id: check-yaml + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + exclude: | + (?x)^( + ^.*libcudacxx/cmake/config\.guess$ + ) + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v20.1.7 + hooks: + - id: clang-format + types_or: [file] + files: | + (?x)^( + ^.*\.c$| + ^.*\.cpp$| + ^.*\.cu$| + ^.*\.cuh$| + ^.*\.cxx$| + ^.*\.h$| + ^.*\.hpp$| + ^.*\.inl$| + ^.*\.mm$| + ^libcudacxx/include/.*/[^.]*$ + ) + args: ["-fallback-style=none", "-style=file", "-i"] + + # TODO/REMINDER: add the Ruff vscode extension to the devcontainers + # Ruff, the Python auto-correcting linter/formatter written in Rust + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.2 + hooks: + - id: ruff # linter + - id: ruff-format # formatter + + # CMake formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.23.1 + hooks: + - id: gersemi + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + additional_dependencies: [tomli] + args: ["--toml", "pyproject.toml", "-I", ".codespell-ignore.txt"] + exclude: | + (?x)^( + build| + CITATION.md + ) + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.16.1' + hooks: + - id: mypy + # numpy is intentionally not installed here: numpy>=2.3 ships PEP 695 + # `type` statements in its stubs that mypy rejects under + # python_version=3.10, and numpy<2.3 has no wheels for the Python that + # pre-commit.ci runs. numpy is instead ignored in the mypy config. + additional_dependencies: [types-cachetools] + args: ["--config-file=python/cuda_cccl/pyproject.toml", + "python/cuda_cccl/cuda/compute/"] + pass_filenames: false + + - repo: https://github.com/sirosen/texthooks + rev: 0.7.1 + hooks: + - id: fix-smartquotes + - id: fix-spaces + - id: forbid-bidi-controls + - id: fix-ligatures + + - repo: local + hooks: + - id: check-shebang + name: check-shebang + entry: ci/util/pre-commit/check_shebang.py + language: python + types: [shell] + exclude: | + (?x)^( + ^.*libcudacxx/cmake/config\.guess$ + ) + + - id: check-cub-test-macros + name: require CUB test memory classification + entry: ci/util/pre-commit/check_cub_test_macros.py + language: python + files: ^(cub/test/.*\.(cu|cuh|h)|ci/util/pre-commit/check_cub_test_macros\.py)$ + types: [file] + + - id: unprintable-unicode + name: unprintable-unicode + entry: ci/util/pre-commit/strip_unprintable.py + language: python + types: [text] + +default_language_version: + python: python3 diff --git a/cccl_upstream/CITATION.md b/cccl_upstream/CITATION.md new file mode 100644 index 00000000..de886a97 --- /dev/null +++ b/cccl_upstream/CITATION.md @@ -0,0 +1,12 @@ +# Citation Guide + +## To Cite CCCL +If you use CCCL in a publication, please use citations in the following format (BibTeX entry for LaTeX): +```tex +@Manual{, + title = {{CCCL}: {CUDA} {C++} {C}ore {L}ibraries}, + author = {{CCCL Development Team}}, + year = {2023}, + url = {https://github.com/NVIDIA/cccl}, +} +``` diff --git a/cccl_upstream/CLAUDE.md b/cccl_upstream/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/cccl_upstream/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/cccl_upstream/CODE_OF_CONDUCT.md b/cccl_upstream/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..c09b71de --- /dev/null +++ b/cccl_upstream/CODE_OF_CONDUCT.md @@ -0,0 +1,84 @@ +# Contributor Covenant Code of Conduct + +## Overview + +Define the code of conduct followed and enforced for CCCL. + +### Intended audience + +Community | Developers | Project Leads + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting GitHub_Conduct@nvidia.com. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate +to the circumstances. The project team is obligated to maintain confidentiality with +regard to the reporter of an incident. Further details of specific enforcement policies +may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/cccl_upstream/CONTRIBUTING.md b/cccl_upstream/CONTRIBUTING.md new file mode 100644 index 00000000..ce9efa07 --- /dev/null +++ b/cccl_upstream/CONTRIBUTING.md @@ -0,0 +1,281 @@ + +# Contributing to CCCL + +Thank you for your interest in contributing to the CUDA Core Compute Libraries (CCCL)! + +Looking for ideas for your first contribution? Check out: ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nvidia/cccl/good%20first%20issue) + + +## Getting Started + +1. **Fork & Clone the Repository**: + + Fork the [CCCL GitHub Repository](https://github.com/nvidia/cccl) and clone the fork. For more information, check [GitHub's documentation on forking](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [cloning a repository](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository). + +2. **Set up Development Environment**: + + CCCL uses Development Containers to provide a consistent development environment for both local development and CI. Contributors are strongly encouraged to use these containers as they simplify environment setup. See the [Dev Containers guide](.devcontainer/README.md) for instructions on how to quickly get up and running using dev containers with or without VSCode. + +## Making Changes + +1. **Create a New Branch**: + + ```bash + git checkout -b your-feature-branch + ``` + +2. **Make Changes**. + +3. **Build and Test**: + + Ensure changes don't break existing functionality by building and running tests. + + ```bash + ./ci/build_[thrust|cub|libcudacxx].sh -cxx -std -arch + + # test implies build + ./ci/test_[thrust|cub|libcudacxx].sh -cxx -std -arch + ``` + + For more details on building and testing, refer to the [Building and Testing](#building-and-testing) section below. + +4. **Commit Changes**: + + ```bash + git commit -m "Brief description of the change" + ``` + +### Developer Guides + +For more information about design and development practices for each CCCL component, refer to the following developer guides: + +#### CUB + +- [CUB Developer Guide](docs/cub/developer_overview.rst) - General overview of the design of CUB internals +- [CUB Tests](docs/cub/test_overview.rst) - Overview of how to write CUB unit tests +- [CUB Benchmarks](docs/cub/benchmarking.rst) - Overview of CUB's performance benchmarks +- [CUB Tunings](docs/cub/tuning.rst) - Overview of CUB's performance tuning infrastructure + +#### Thrust + +Coming soon! + +#### libcudacxx + +Coming soon! + +## Building and Testing + +CCCL components are header-only libraries. This means there isn't a traditional build process for the library itself. However, before submitting contributions, it's a good idea to [build and run tests](#developer-guides). + +There are multiple options for building and running our tests. Which option you choose depends on your preferences and whether you are using [CCCL's DevContainers](.devcontainer/README.md) (highly recommended!). + +### Using Manual Build Scripts + +#### Building + +Use the build scripts provided in the `ci/` directory to build tests for each component. Building tests does not require a GPU. + +```bash + ci/build_[thrust|cub|libcudacxx].sh -cxx -std -arch +``` +- **HOST_COMPILER**: The desired host compiler (e.g., `g++`, `clang++`). +- **CXX_STANDARD**: The C++ standard version (e.g., `17`, `20`). +- **GPU_ARCHS**: A semicolon-separated list of CUDA GPU architectures (e.g., `"70;85;90"`). This uses the same syntax as CMake's [CUDA_ARCHITECTURES](https://cmake.org/cmake/help/latest/prop_tgt/CUDA_ARCHITECTURES.html#prop_tgt:CUDA_ARCHITECTURES): + - `70` - both PTX and SASS + - `70-real` - SASS only + - `70-virtual` - PTX only + +**Example:** +```bash +./ci/build_cub.sh -cxx g++ -std 17 -arch "70;75;80-virtual" +``` + +#### Testing + +Use the test scripts provided in the `ci/` directory to run tests for each component. These take the same arguments as the build scripts and will automatically build the tests if they haven't already been built. Running tests requires a GPU. + +```bash + ci/test_[thrust|cub|libcudacxx].sh -cxx -std -arch +``` + +**Example:** + +```bash +./ci/test_cub.sh -cxx g++ -std 17 -arch "70;75;80-virtual" +``` + +### Using CMake Presets + +[CMake Presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html) are a set of configurations defined in a JSON file that specify project-wide build details for CMake. They provide a standardized and sharable way to configure, build, and test projects across different platforms and development environments. Presets are available from CMake versions 3.19 and later. + +There are three kinds of Presets + +- Configure Presets: specify options for the `cmake` command, + +- Build Presets: specify options for the `cmake --build` command, + +- Test Presets: specify options for the `ctest` command. + +In CCCL we provide many presets to be used out of the box. You can find the complete list in our corresponding [CMakePresets.json](./CMakePresets.json) file. + +These commands can be used to get lists of the configure, build, and test presets. + +```bash +cmake --list-presets # Configure presets +cmake --build --list-presets # Build presets +ctest --list-presets # Test presets +``` + +While there is a lot of overlap, there may be differences between the configure, build, and test presets to support various testing workflows. + +The `dev` presets are intended as a base for general development while the others are useful for replicating CI failures. + +#### Using CMake Presets via Command Line + +CMake automatically generates the preset build directories. You can configure, build and test for a specific preset (e.g. `thrust-cpp17`) via cmake from the root directory by appending `--preset=thrust-cpp17` to the corresponding commands. For example: + +```bash +cmake --preset=thrust-cpp17 +cmake --build --preset=thrust-cpp17 +ctest --preset=thrust-cpp17 +``` + +That will create `build//thrust-cpp17/` and build everything in there. The devcontainer name is inserted automatically on devcontainer builds to keep build artifacts separate for the different toolchains. + +It's also worth mentioning that additional cmake options can still be passed in and will override the preset settings. + +As a common example, the presets are currently always `60;70;80` for `CMAKE_CUDA_ARCHITECTURES`, but this can be overridden at configure time with something like: + +```bash +cmake --preset=thrust-cpp20 "-DCMAKE_CUDA_ARCHITECTURES=89" +``` + +> **Note**: Either using the `cmake` command from within the root directory or from within the build directory works, but will behave in slightly different ways. Building and running tests from the build directory will compile every target and run all of the tests configured in the configure step. Doing so from the root directory using the `--preset=` option will build and run a subset of configured targets and tests. + +#### Using CMake Presets via VS Code GUI extension (Recommended when using DevContainers) + +The recommended way to use CMake Presets is via the VS Code extension [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools), already included in [CCCL's DevContainers](.devcontainer/README.md). As soon as you install the extension you would be able to see the sidebar menu below. + + ![cmaketools sidebar](/.devcontainer/img/cmaketools_sidebar.png) + +You can specify the desired CMake Preset by clicking the "Select Configure Preset" button under the "Configure" node (see image below). + + ![cmaketools presets](.devcontainer/img/cmaketools_presets.png) + +After that you can select the default build target from the "Build" node. As soon as you expand it, a list will appear with all the available targets that are included within the preset you selected. For example if you had selected the `all-dev` preset VS Code will display all the available targets we have in cccl. + + ![cmaketools presets](.devcontainer/img/cmaketools_targets.png) + +You can build the selected target by pressing the gear button ![gear](.devcontainer/img/build_button.png) at the bottom of the VS Code window. + +Alternatively you can select the desired target from either the "Debug" or "Launch" drop down menu (for debugging or running correspondingly). In that case after you select the target and either press "Run" ![run](.devcontainer/img/run.png) or "Debug" ![debug](.devcontainer/img/debug.png) the target will build on its own before running without the user having to build it explicitly from the gear button. + +--- + +We encourage users who want to debug device code to install the [Nsight Visual Studio Code Edition extension](https://marketplace.visualstudio.com/items?itemName=NVIDIA.nsight-vscode-edition) that enables the VS Code frontend for `cuda-gdb`. To use it you should launch from the sidebar menu instead of pressing the "Debug" button from the bottom menu. + +![nsight](.devcontainer/img/nsight.png) + +## Creating a Pull Request + +1. Push changes to your fork +2. Create a pull request targeting the `main` branch of the original CCCL repository. Refer to [GitHub's documentation](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on creating a pull request. +3. Describe the purpose and context of the changes in the pull request description. + +### Documentation Preview + +Documentation previews allow reviewers to see how changes will appear on the live documentation site before merging. Previews are automatically generated for all pull requests and updated with every commit. To skip building documentation for a PR, include `[skip-docs]` in your commit message. + +The preview URL will be posted as a comment on your PR and automatically cleaned up when the PR is closed. + +### Checking for Performance Regressions + +Performance stability is a key goal for CCCL, especially for `Device*` algorithms in CUB. When modifying any functionality that could impact these algorithms, contributors are encouraged to verify that no performance regressions occur. + +This verification is a two-step process: + +1. Determine whether your changes affect the generated SASS code (details on how to do this are provided below). +2. If the generated SASS code changes, run the benchmarks (see [CUB Benchmarks](docs/cub/benchmarking.rst)) to quantify potential performance implications. + +**Steps to check whether your changes generate different SASS code:** + +1. Identify the `Device*` algorithm(s) that may be affected by the change. This isn't always straightforward, and you will need to confirm whether any of the CUB algorithms depend on components modified by your changes. If your changes affect only certain GPU architectures, make sure those architectures are included in the list of architectures used during compilation (for example, by specifying them with the `-arch` flag when using the build scripts, or with `-DCMAKE_CUDA_ARCHITECTURES` when building with CMake). +2. Navigate to the build directory, compile the benchmarks for the specific `Device*` algorithm(s) identified in step 1, and dump the SASS code. For example: `ninja cub.bench.radix_sort.keys.base && cuobjdump -sass ./bin/cub.bench.radix_sort.keys.base |c++filt > ./radix_sort.keys_after.sass`. +3. Check out the `main` branch to compare against the baseline SASS code: `git checkout $(git merge-base HEAD upstream/main)` +4. Dump the SASS code emitted on the `main` branch. For example: `ninja cub.bench.radix_sort.keys.base && cuobjdump -sass ./bin/cub.bench.radix_sort.keys.base |c++filt > ./radix_sort.keys_before.sass`. +5. Check whether there are differences in the generated SASS output: `git diff --text --no-index --word-diff radix_sort.keys_before.sass radix_sort.keys_after.sass` + +## Code Formatting (pre-commit hooks) + +CCCL uses [pre-commit](https://pre-commit.com/) to execute all code linters and formatters. These +tools ensure a consistent coding style throughout the project. Using pre-commit ensures that linter +versions and options are aligned for all developers. Additionally, there is a CI check in place to +enforce that committed code follows our standards. + +The linters used by CCCL are listed in `.pre-commit-config.yaml`. +For example, C++ and CUDA code is formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html). + +To use `pre-commit`, install via `conda` or `pip`: + +```bash +conda config --add channels conda-forge +conda install pre-commit +``` + +```bash +pip install pre-commit +``` + +Then run pre-commit hooks before committing code: + +```bash +pre-commit run +``` + +By default, pre-commit runs on staged files (only changes and additions that will be committed). +To run pre-commit checks on all files, execute: + +```bash +pre-commit run --all-files +``` + +Optionally, you may set up the pre-commit hooks to run automatically when you make a git commit. This can be done by running: + +```bash +pre-commit install +``` + +Now code linters and formatters will be run each time you commit changes. + +You can skip these checks with `git commit --no-verify` or with the short version `git commit -n`. + +## Secret Scanning + +The `secret-scan-trufflehog` pre-commit hook scans staged files and installs TruffleHog on first run (use Git Bash on Windows). If it flags a secret, remove it before committing, or contact a maintainer if it's a false positive. Secrets are also scanned server-side in CI on `main`. + +## Continuous Integration (CI) + +CCCL's CI pipeline tests across various CUDA versions, compilers, and GPU architectures. +For external contributors, the CI pipeline will not begin until a maintainer leaves an `/ok to test` comment. For members of the NVIDIA GitHub enterprise, the CI pipeline will begin immediately. +For a detailed overview of CCCL's CI, see [CI overview](docs/infrastructure/ci/references/ci_overview.rst). + +There is a CI check for pre-commit, called [pre-commit.ci](pre-commit.ci). +This enforces that all linters (such as `clang-format`) pass. +If pre-commit.ci is failing, you can comment `pre-commit.ci autofix` on a pull request to trigger the auto-fixer. +The auto-fixer will push a commit to your pull request that applies changes made by pre-commit hooks. + +## Review Process + +Once submitted, maintainers will be automatically assigned to review the pull request. They might suggest changes or improvements. Constructive feedback is a part of the collaborative process, aimed at ensuring the highest quality code. + +For constructive feedback and effective communication during reviews, we recommend following [Conventional Comments](https://conventionalcomments.org/). + +Further recommended reading for successful PR reviews: + +- [How to Do Code Reviews Like a Human (Part One)](https://mtlynch.io/human-code-reviews-1/) +- [How to Do Code Reviews Like a Human (Part Two)](https://mtlynch.io/human-code-reviews-2/) + +## Thank You + +Your contributions enhance CCCL for the entire community. We appreciate your effort and collaboration! diff --git a/cccl_upstream/SECURITY.md b/cccl_upstream/SECURITY.md new file mode 100644 index 00000000..241adf66 --- /dev/null +++ b/cccl_upstream/SECURITY.md @@ -0,0 +1,14 @@ +## Security + +NVIDIA is dedicated to the security and trust of our software products and services, including all source code repositories managed through our organization. + +If you need to report a security issue, please use the appropriate contact points outlined below. **Please do not report security vulnerabilities through GitHub.** + +## Reporting Potential Security Vulnerability in an NVIDIA Product + +To report a potential security vulnerability in any NVIDIA product: +- Web: [Security Vulnerability Submission Form](https://www.nvidia.com/object/submit-security-vulnerability.html) +- E-Mail: psirt@nvidia.com + - We encourage you to use the following PGP key for secure email communication: [NVIDIA public PGP Key for communication](https://www.nvidia.com/en-us/security/pgp-key) + - Please include the following information: + - Product/Driver name and version/branch that contains the vulnerability diff --git a/cccl_upstream/ci/CMakeLists.txt b/cccl_upstream/ci/CMakeLists.txt new file mode 100644 index 00000000..0130ac18 --- /dev/null +++ b/cccl_upstream/ci/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory("test") diff --git a/cccl_upstream/ci/bench.template.yaml b/cccl_upstream/ci/bench.template.yaml new file mode 100644 index 00000000..02f60b62 --- /dev/null +++ b/cccl_upstream/ci/bench.template.yaml @@ -0,0 +1,70 @@ +# # CCCL PR benchmark request config. +# +# ## Overview: +# +# This file is used to request benchmark comparisons in PR CI. +# +# This file must match ci/bench.template.yaml to merge. +# CI branch protections will fail if they differ. Reset before merging. +# +# To update the defaults (e.g. new GPU pools), modify both this file and +# ci/bench.template.yaml together in the same PR. +# +# !! Strongly consider appending the following to your **commit messages** while benchmarking. !! +# This prevents wasteful non-benchmark CI jobs if they are not needed. +# +# [bench-only] +# +# To skip compile-time benchmark telemetry on unrelated changes, use: +# +# [skip-compile-time-bench] +# +# ## Quick start: +# +# 1. Add one or more benchmark regexes under benchmarks.filters.cub and/or +# benchmarks.filters.python. +# 2. Enable at least one GPU by uncommenting or adding entries in benchmarks.gpus. +# 3. Push and inspect the dispatched benchmark jobs/artifacts. +# 4. Remove/reset benchmark-request edits before final merge. + +benchmarks: + + # Benchmark filters grouped by project. + filters: + # CUB C++ benchmark filters (regex matched against ninja target names). + cub: + # Examples: + # - '^cub\.bench\.for_each\.base' + # - '^cub\.bench\.reduce\.(sum|min)\.' + + # Python benchmark filters (regex matched against paths under benchmarks/). + python: + # Examples: + # - 'compute/reduce/sum\.py' + # - 'compute/transform/.*\.py' + + # Select GPUs. These are limited and shared, be intentional and conservative. + gpus: + # - "t4" # sm_75, 16 GB + # - "rtx2080" # sm_75, 8 GB + # - "rtxa6000" # sm_86, 48 GB + # - "l4" # sm_89, 24 GB + # - "rtx4090" # sm_89, 24 GB + # - "h100" # sm_90, 80 GB + # - "rtxpro6000" # sm_120 + + # Extra .devcontainer/launch.sh -d args + # launch_args: "--cuda 13.3 --host gcc14" + launch_args: "" # Latest nvcc + gcc + + # Advanced: + base_ref: "origin/main" + test_ref: "HEAD" + arch: "native" + nvbench_args: >- + --timeout 30 + --skip-time 15e-6 + --stopping-criterion entropy + --throttle-threshold 90 + --throttle-recovery-delay 0.15 + nvbench_compare_args: "" diff --git a/cccl_upstream/ci/bench.yaml b/cccl_upstream/ci/bench.yaml new file mode 100644 index 00000000..02f60b62 --- /dev/null +++ b/cccl_upstream/ci/bench.yaml @@ -0,0 +1,70 @@ +# # CCCL PR benchmark request config. +# +# ## Overview: +# +# This file is used to request benchmark comparisons in PR CI. +# +# This file must match ci/bench.template.yaml to merge. +# CI branch protections will fail if they differ. Reset before merging. +# +# To update the defaults (e.g. new GPU pools), modify both this file and +# ci/bench.template.yaml together in the same PR. +# +# !! Strongly consider appending the following to your **commit messages** while benchmarking. !! +# This prevents wasteful non-benchmark CI jobs if they are not needed. +# +# [bench-only] +# +# To skip compile-time benchmark telemetry on unrelated changes, use: +# +# [skip-compile-time-bench] +# +# ## Quick start: +# +# 1. Add one or more benchmark regexes under benchmarks.filters.cub and/or +# benchmarks.filters.python. +# 2. Enable at least one GPU by uncommenting or adding entries in benchmarks.gpus. +# 3. Push and inspect the dispatched benchmark jobs/artifacts. +# 4. Remove/reset benchmark-request edits before final merge. + +benchmarks: + + # Benchmark filters grouped by project. + filters: + # CUB C++ benchmark filters (regex matched against ninja target names). + cub: + # Examples: + # - '^cub\.bench\.for_each\.base' + # - '^cub\.bench\.reduce\.(sum|min)\.' + + # Python benchmark filters (regex matched against paths under benchmarks/). + python: + # Examples: + # - 'compute/reduce/sum\.py' + # - 'compute/transform/.*\.py' + + # Select GPUs. These are limited and shared, be intentional and conservative. + gpus: + # - "t4" # sm_75, 16 GB + # - "rtx2080" # sm_75, 8 GB + # - "rtxa6000" # sm_86, 48 GB + # - "l4" # sm_89, 24 GB + # - "rtx4090" # sm_89, 24 GB + # - "h100" # sm_90, 80 GB + # - "rtxpro6000" # sm_120 + + # Extra .devcontainer/launch.sh -d args + # launch_args: "--cuda 13.3 --host gcc14" + launch_args: "" # Latest nvcc + gcc + + # Advanced: + base_ref: "origin/main" + test_ref: "HEAD" + arch: "native" + nvbench_args: >- + --timeout 30 + --skip-time 15e-6 + --stopping-criterion entropy + --throttle-threshold 90 + --throttle-recovery-delay 0.15 + nvbench_compare_args: "" diff --git a/cccl_upstream/ci/bench/README.md b/cccl_upstream/ci/bench/README.md new file mode 100644 index 00000000..51de0faa --- /dev/null +++ b/cccl_upstream/ci/bench/README.md @@ -0,0 +1,82 @@ +# Benchmark Compare Scripts + +This directory contains the scripts used by `.github/workflows/bench.yml` to compare benchmark results between two code states. + +## Scripts + +- `ci/bench/bench.sh`: CI-oriented wrapper that calls `ci/bench/compare_git_refs.sh`. +- `ci/bench/compare_git_refs.sh`: checks out `` and `` in temporary worktrees, then forwards all remaining args to `ci/bench/compare_paths.sh`. +- `ci/bench/compare_paths.sh`: configures/builds/runs CUB benchmarks and/or Python benchmarks in two source trees and runs comparison tools on produced JSON outputs. +- `ci/bench/parse_bench_matrix.sh`: parses `ci/bench.yaml` and emits a dispatch matrix JSON object for `.github/workflows/bench.yml`. + +## Usage + +Compare CUB benchmarks between two refs: + +```bash +"./ci/bench/bench.sh" "origin/main" "HEAD" \ + --cub-filter "^cub\\.bench\\.copy\\.memcpy\\.base$" +``` + +Compare Python benchmarks between two refs: + +```bash +"./ci/bench/bench.sh" "origin/main" "HEAD" \ + --python-filter "compute/reduce/sum\\.py" +``` + +Run both CUB and Python benchmarks: + +```bash +"./ci/bench/bench.sh" \ + "origin/main" \ + "HEAD" \ + --arch "native" \ + --nvbench-args "..." \ + --cub-filter "^cub\\.bench\\.reduce\\..*$" \ + --python-filter "compute/reduce/sum\\.py" +``` + +Compare already checked-out trees: + +```bash +"./ci/bench/compare_paths.sh" \ + "/path/to/base/cccl" \ + "/path/to/test/cccl" \ + --arch "native" \ + --cub-filter "^cub\\.bench\\.copy\\.memcpy\\.base$" \ + --python-filter "compute/transform/.*\\.py" +``` + +## Workflow Inputs + +In `.github/workflows/bench.yml`: + +- If `raw_args` is non-empty, it is parsed and passed directly to `ci/bench/bench.sh`. +- Otherwise, args are assembled from `base_ref`, `test_ref`, `arch`, `cub_filters`, `python_filters`, `nvbench_args`, and `nvbench_compare_args`. +- CUB filters are passed as `--cub-filter` flags. Python filters are passed as `--python-filter` flags. +- Malformed quoted input (for example unmatched quotes) fails the workflow step. + +## Python Benchmarks + +Python benchmarks live under `python/cuda_cccl/benchmarks/` and use `cuda.bench` (the Python nvbench bindings). Each benchmark script outputs nvbench-compatible JSON. + +For Python benchmarks, `compare_paths.sh`: + +1. Creates isolated virtual environments for base and test trees. +2. Installs `cuda-cccl[bench-cuXX]` (editable, from each worktree), which pulls in `cuda-bench`, `cupy`, and all other benchmark dependencies. +3. Runs matching benchmark scripts in each venv. +4. Compares results using `nvbench-compare` (installed with `cuda-bench`). + +Python filters are regex patterns matched against relative paths under `python/cuda_cccl/benchmarks/`, for example: +- `compute/reduce/sum\.py` — single benchmark +- `compute/transform/.*\.py` — all transform benchmarks + +## Artifacts + +`compare_paths.sh` writes a run directory under `${CCCL_BENCH_ARTIFACT_ROOT:-$(pwd)/bench-artifacts}` containing: + +- per-target JSON and markdown outputs for base/test runs, +- grouped build logs (`build.base.log`, `build.test.log`), per-target run logs, and per-target compare logs (`compare..log`), +- Python venv setup logs (`py.venv.base.log`, `py.venv.test.log`), +- `summary.md` with run metadata and per-target collapsible full compare reports. diff --git a/cccl_upstream/ci/bench/bench.sh b/cccl_upstream/ci/bench/bench.sh new file mode 100755 index 00000000..5d08f25d --- /dev/null +++ b/cccl_upstream/ci/bench/bench.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +die() { + local message="$1" + local code="${2:-2}" + echo "${message}" >&2 + exit "${code}" +} + +usage() { + cat < [compare_paths args...] + +Wrapper for ci/bench/compare_git_refs.sh. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +bench_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${bench_dir}/compare_git_refs.sh" "$@" diff --git a/cccl_upstream/ci/bench/compare_git_refs.sh b/cccl_upstream/ci/bench/compare_git_refs.sh new file mode 100755 index 00000000..a20aacd9 --- /dev/null +++ b/cccl_upstream/ci/bench/compare_git_refs.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +set -euo pipefail + +die() { + local message="$1" + local code="${2:-2}" + echo "${message}" >&2 + exit "${code}" +} + +usage() { + cat < [compare_paths args...] + +Compare benchmark performance between two git refs from the current CCCL repo. +Each ref is checked out in an isolated worktree and compared via compare_paths.sh. +EOF +} + +display_label_for_ref() { + local ref="$1" + local short_sha="$2" + if [[ "${ref}" =~ ^[0-9a-fA-F]{7,40}$ ]]; then + printf "%s" "${short_sha}" + return 0 + fi + if [[ "${ref}" == "HEAD" ]]; then + printf "%s" "${short_sha}" + return 0 + fi + printf "%s" "${ref}" +} + +resolve_ref_to_commit() { + local repo_root="$1" + local ref="$2" + local remote="" + local branch="" + local alternate_ref="" + + if git -C "${repo_root}" rev-parse --verify "${ref}^{commit}" >/dev/null 2>&1; then + git -C "${repo_root}" rev-parse --verify "${ref}^{commit}" + return 0 + fi + + if [[ "${ref}" =~ ^([^/]+)/(.+)$ ]] && git -C "${repo_root}" remote get-url "${BASH_REMATCH[1]}" >/dev/null 2>&1; then + remote="${BASH_REMATCH[1]}" + branch="${BASH_REMATCH[2]}" + git -C "${repo_root}" fetch --no-tags "${remote}" \ + "+refs/heads/${branch}:refs/remotes/${remote}/${branch}" >/dev/null 2>&1 || true + elif [[ "${ref}" != refs/* ]]; then + # Try unqualified refs as origin branches/tags. + git -C "${repo_root}" fetch --no-tags origin \ + "+refs/heads/${ref}:refs/remotes/origin/${ref}" >/dev/null 2>&1 || true + git -C "${repo_root}" fetch --no-tags origin \ + "refs/tags/${ref}:refs/tags/${ref}" >/dev/null 2>&1 || true + alternate_ref="origin/${ref}" + fi + + # Final best-effort fetch for raw refs (e.g. refs/pull/* or specific SHAs). + git -C "${repo_root}" fetch --no-tags origin "${ref}" >/dev/null 2>&1 || true + + if git -C "${repo_root}" rev-parse --verify "${ref}^{commit}" >/dev/null 2>&1; then + git -C "${repo_root}" rev-parse --verify "${ref}^{commit}" + return 0 + fi + + if [[ -n "${alternate_ref}" ]] && git -C "${repo_root}" rev-parse --verify "${alternate_ref}^{commit}" >/dev/null 2>&1; then + git -C "${repo_root}" rev-parse --verify "${alternate_ref}^{commit}" + return 0 + fi + + return 1 +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ "$#" -lt 2 ]]; then + usage + exit 2 +fi + +base_ref="$1" +test_ref="$2" +shift 2 +compare_paths_args=("$@") + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${ci_dir}/../.." && pwd)" + +if [[ ! -f "${repo_root}/cccl-version.json" ]]; then + die "This script must run from a CCCL checkout." +fi + +if ! base_commit="$(resolve_ref_to_commit "${repo_root}" "${base_ref}")"; then + die "Unable to resolve base ref: ${base_ref}" +fi +if ! test_commit="$(resolve_ref_to_commit "${repo_root}" "${test_ref}")"; then + die "Unable to resolve test ref: ${test_ref}" +fi + +base_short_sha="$(git -C "${repo_root}" rev-parse --short=12 "${base_commit}")" +test_short_sha="$(git -C "${repo_root}" rev-parse --short=12 "${test_commit}")" +base_label="$(display_label_for_ref "${base_ref}" "${base_short_sha}")" +test_label="$(display_label_for_ref "${test_ref}" "${test_short_sha}")" + +worktree_root="$(mktemp -d "/tmp/cccl-bench-worktrees-XXXXXX")" +base_path="${worktree_root}/base" +test_path="${worktree_root}/test" + +cleanup() { + git -C "${repo_root}" worktree remove --force "${base_path}" >/dev/null 2>&1 || true + git -C "${repo_root}" worktree remove --force "${test_path}" >/dev/null 2>&1 || true + rm -rf "${worktree_root}" +} +trap cleanup EXIT + +echo "Creating worktree for base ref: ${base_ref}" +git -C "${repo_root}" worktree add --detach "${base_path}" "${base_commit}" >/dev/null + +echo "Creating worktree for test ref: ${test_ref}" +git -C "${repo_root}" worktree add --detach "${test_path}" "${test_commit}" >/dev/null + +compare_cmd=( + "${ci_dir}/compare_paths.sh" + "${base_path}" + "${test_path}" + "${compare_paths_args[@]}" +) + +CCCL_BENCH_BASE_LABEL="${base_label}" \ +CCCL_BENCH_TEST_LABEL="${test_label}" \ + "${compare_cmd[@]}" diff --git a/cccl_upstream/ci/bench/compare_paths.sh b/cccl_upstream/ci/bench/compare_paths.sh new file mode 100755 index 00000000..2eb45a4a --- /dev/null +++ b/cccl_upstream/ci/bench/compare_paths.sh @@ -0,0 +1,1048 @@ +#!/usr/bin/env bash + +set -euo pipefail + +die() { + local message="$1" + local code="${2:-2}" + echo "${message}" >&2 + exit "${code}" +} + +usage() { + cat < \ + [--cub-filter ""] \ + [--python-filter ""] \ + [--arch ""] \ + [--nvbench-args ""] \ + [--nvbench-compare-args ""] + +Compare benchmark performance between two checked-out CCCL trees. + +At least one --cub-filter or --python-filter must be provided. +CUB filters are regex patterns matched against ninja target names. +Python filters are regex patterns matched against benchmark script paths +under python/cuda_cccl/benchmarks/ (e.g. compute/reduce/sum.py). + +Arguments: + Path to baseline CCCL source tree. + Path to comparison CCCL source tree. + +Options: + --cub-filter CUB benchmark regex filter (repeatable). + --python-filter Python benchmark regex filter (repeatable). + --arch CMAKE_CUDA_ARCHITECTURES for CUB builds. + --nvbench-args Extra args passed to benchmark binaries/scripts. + --nvbench-compare-args Extra args passed to nvbench_compare. + +Environment: + CCCL_BENCH_ARTIFACT_ROOT Root directory for outputs. + Default: "\$(pwd)/bench-artifacts" + CCCL_BENCH_ARTIFACT_TAG Optional explicit artifact directory name. + CCCL_BENCH_BASE_LABEL Optional label used in auto-generated artifact names. + CCCL_BENCH_TEST_LABEL Optional label used in auto-generated artifact names. + CCCL_BENCH_BASE_BUILD_DIR Optional preconfigured build tree for base path. + CCCL_BENCH_TEST_BUILD_DIR Optional preconfigured build tree for test path. + If either is set, both must be set. + CCCL_BENCH_GPU_NAME Optional GPU name included in artifact directory names. + CCCL_BENCH_BUILD_ROOT Root directory for generated build trees. + Default: "/tmp/cccl-bench-builds" +EOF +} + +sanitize_label() { + local label="$1" + label="${label//[^a-zA-Z0-9._-]/_}" + label="${label#_}" + label="${label%_}" + label="${label:-unknown}" + printf "%s" "${label}" +} + +resolve_repo_label() { + local repo_path="$1" + local branch="" + branch="$(git -C "${repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [[ -n "${branch}" && "${branch}" != "HEAD" ]]; then + printf "%s" "${branch}" + return 0 + fi + local short_sha="" + short_sha="$(git -C "${repo_path}" rev-parse --short=12 HEAD 2>/dev/null || true)" + if [[ -n "${short_sha}" ]]; then + printf "%s" "${short_sha}" + return 0 + fi + basename "${repo_path}" +} + +validate_repo_path() { + local repo_path="$1" + if [[ ! -d "${repo_path}" ]]; then + die "Path does not exist: ${repo_path}" + fi + if [[ ! -f "${repo_path}/cccl-version.json" ]]; then + die "Path is not a CCCL source tree: ${repo_path}" + fi +} + +validate_filter_array() { + local -n _validate_filters_ref="$1" + local label="$2" + local filter="" + for filter in "${_validate_filters_ref[@]}"; do + grep -Eq -- "${filter}" <<< "" >/dev/null 2>&1 || { + [[ "$?" -eq 1 ]] || die "Invalid ${label} regex filter: ${filter}" + } + done +} + +print_shell_command() { + # Print a shell-escaped command line so it can be copied and re-run directly. + # Usage: print_shell_command [--env "VAR=value" ...] cmd [args...] + local -a env_prefixes=() + while [[ "$#" -gt 0 && "$1" == --env ]]; do + shift + env_prefixes+=("$1") + shift + done + printf '$' + local item="" + for item in "${env_prefixes[@]}"; do + printf ' %q' "${item}" + done + for item in "$@"; do + printf ' %q' "${item}" + done + printf '\n' +} + +# ============================================================================ +# CUB helpers +# ============================================================================ + +configure_build_tree() { + local src_path="$1" + local build_path="$2" + local side="$3" + local log_path="$4" + local target_arch="$5" + local -a cmake_cmd + cmake_cmd=(cmake --preset "cub-benchmark" -S "${src_path}" -B "${build_path}") + if [[ -n "${target_arch}" ]]; then + cmake_cmd+=("-DCMAKE_CUDA_ARCHITECTURES=${target_arch}") + fi + run_grouped_logged_command "[configure:${side}]" "${log_path}" "${cmake_cmd[@]}" +} + +validate_build_dir() { + local build_path="$1" + local label="$2" + if [[ ! -d "${build_path}" ]]; then + die "Configured ${label} build tree does not exist: ${build_path}" + fi + if [[ ! -f "${build_path}/build.ninja" ]]; then + die "Configured ${label} build tree is missing build.ninja: ${build_path}" + fi +} + +list_all_benchmark_targets() { + local build_path="$1" + ninja -C "${build_path}" -t targets all \ + | awk -F':' '/^.*\.bench\./ { print $1 }' \ + | sort -u +} + +target_matches_filters() { + local target="$1" + local filter="" + if [[ "${#FILTERS[@]}" -eq 0 ]]; then + return 0 + fi + for filter in "${FILTERS[@]}"; do + if grep -Eq -- "${filter}" <<< "${target}"; then + return 0 + fi + done + return 1 +} + +resolve_compare_script() { + local build_path="$1" + local nvbench_src="${build_path}/_deps/nvbench-src" + local candidate="" + # Diff versions have the script at diff locations: + for candidate in \ + "${nvbench_src}/python/scripts/nvbench_compare.py" \ + "${nvbench_src}/scripts/nvbench_compare.py"; do + if [[ -f "${candidate}" ]]; then + printf "%s" "${candidate}" + return 0 + fi + done + return 1 +} + +run_target_for_side() { + local side="$1" + local build_path="$2" + local target="$3" + local json_path="$4" + local md_path="$5" + local run_log="$6" + local binary_path="${build_path}/bin/${target}" + local -a bench_cmd + + if [[ ! -x "${binary_path}" ]]; then + echo "Benchmark binary missing: ${binary_path}" >&2 + return 127 + fi + + bench_cmd=( + "${binary_path}" + -d 0 + "${NVBENCH_RUN_ARGS[@]}" + --json "${json_path}" + --md "${md_path}" + ) + + run_grouped_logged_command \ + "[run:${side}] ${target}" \ + "${run_log}" \ + "${bench_cmd[@]}" +} + +select_targets() { + local base_build_path="$1" + local test_build_path="$2" + local -n selected_targets_ref="$3" + local -a base_targets + local -a test_targets + local -a common_targets + local target="" + + mapfile -t base_targets < <(list_all_benchmark_targets "${base_build_path}") + mapfile -t test_targets < <(list_all_benchmark_targets "${test_build_path}") + + if [[ "${#base_targets[@]}" -eq 0 ]]; then + die "No CUB benchmark targets were found in base build tree." 1 + fi + if [[ "${#test_targets[@]}" -eq 0 ]]; then + die "No CUB benchmark targets were found in test build tree." 1 + fi + + mapfile -t common_targets < <( + comm -12 \ + <(printf "%s\n" "${base_targets[@]}" | sort -u) \ + <(printf "%s\n" "${test_targets[@]}" | sort -u) + ) + + selected_targets_ref=() + for target in "${common_targets[@]}"; do + [[ -n "${target}" ]] || continue + if target_matches_filters "${target}"; then + selected_targets_ref+=("${target}") + fi + done + + if [[ "${#selected_targets_ref[@]}" -eq 0 ]]; then + die "No CUB benchmark targets matched the supplied filters." 1 + fi +} + +# ============================================================================ +# Python helpers +# ============================================================================ + +detect_cuda_major_version() { + local cuda_major="" + if command -v nvcc >/dev/null 2>&1; then + cuda_major="$(nvcc --version 2>/dev/null | sed -n 's/.*release \([0-9]*\)\..*/\1/p')" + fi + if [[ -z "${cuda_major}" ]]; then + cuda_major="12" + fi + printf "%s" "${cuda_major}" +} + +python_path_to_target_name() { + local py_path="$1" + # compute/reduce/sum.py -> py.compute.reduce.sum + local name="${py_path%.py}" + name="${name//\//.}" + printf "py.%s" "${name}" +} + +list_all_python_benchmarks() { + local benchmarks_path="$1" + if [[ ! -d "${benchmarks_path}" ]]; then + return 0 + fi + find "${benchmarks_path}" -name '*.py' -type f \ + ! -name 'utils.py' \ + ! -name 'run_benchmarks.py' \ + ! -name 'device_side_benchmark.py' \ + ! -name '__init__.py' \ + ! -path '*/__pycache__/*' \ + -printf '%P\n' \ + | sort -u +} + +python_target_matches_filters() { + local target="$1" + local filter="" + for filter in "${PYTHON_FILTERS[@]}"; do + if grep -Eq -- "${filter}" <<< "${target}"; then + return 0 + fi + done + return 1 +} + +select_python_targets() { + local base_bench_path="$1" + local test_bench_path="$2" + local -n selected_py_targets_ref="$3" + local -a base_py_targets + local -a test_py_targets + local -a common_py_targets + local target="" + + mapfile -t base_py_targets < <(list_all_python_benchmarks "${base_bench_path}") + mapfile -t test_py_targets < <(list_all_python_benchmarks "${test_bench_path}") + + if [[ "${#base_py_targets[@]}" -eq 0 ]]; then + die "No Python benchmark scripts were found in base tree: ${base_bench_path}" 1 + fi + if [[ "${#test_py_targets[@]}" -eq 0 ]]; then + die "No Python benchmark scripts were found in test tree: ${test_bench_path}" 1 + fi + + mapfile -t common_py_targets < <( + comm -12 \ + <(printf "%s\n" "${base_py_targets[@]}" | sort -u) \ + <(printf "%s\n" "${test_py_targets[@]}" | sort -u) + ) + + selected_py_targets_ref=() + for target in "${common_py_targets[@]}"; do + [[ -n "${target}" ]] || continue + if python_target_matches_filters "${target}"; then + selected_py_targets_ref+=("${target}") + fi + done + + if [[ "${#selected_py_targets_ref[@]}" -eq 0 ]]; then + die "No Python benchmark scripts matched the supplied --python-filter patterns." 1 + fi +} + +setup_python_venv() { + local venv_path="$1" + local src_path="$2" + local side="$3" + local log_path="$4" + local cuda_major="$5" + local cuda_cccl_dir="${src_path}/python/cuda_cccl" + + if [[ ! -d "${cuda_cccl_dir}" ]]; then + die "cuda_cccl source directory not found: ${cuda_cccl_dir}" + fi + + local -a setup_cmds + setup_cmds=( + bash -c " + set -euo pipefail + python3 -m venv '${venv_path}' + '${venv_path}/bin/pip' install --upgrade pip + '${venv_path}/bin/pip' install -e '${cuda_cccl_dir}[bench-cu${cuda_major}]' + # nvbench-compare runtime deps (until cuda-bench declares them): + '${venv_path}/bin/pip' install colorama jsondiff tabulate + " + ) + + run_grouped_logged_command \ + "[py-venv:${side}]" \ + "${log_path}" \ + "${setup_cmds[@]}" +} + +run_python_target_for_side() { + local side="$1" + local venv_path="$2" + local script_path="$3" + local json_path="$4" + local md_path="$5" + local run_log="$6" + local -a bench_cmd + + if [[ ! -f "${script_path}" ]]; then + echo "Python benchmark script missing: ${script_path}" >&2 + return 127 + fi + + bench_cmd=( + "${venv_path}/bin/python" + "${script_path}" + -d 0 + "${NVBENCH_RUN_ARGS[@]}" + --json "${json_path}" + --md "${md_path}" + ) + + run_grouped_logged_command \ + "[py-run:${side}] ${script_path##*/benchmarks/}" \ + "${run_log}" \ + "${bench_cmd[@]}" +} + +run_python_compare_target() { + local target_name="$1" + local venv_path="$2" + local base_json="$3" + local test_json="$4" + local compare_out="$5" + local compare_log="$6" + + local label="[py-compare] ${target_name}" + local started_at=0 + local elapsed_s=0 + local rc=0 + local -a compare_cmd + compare_cmd=("${venv_path}/bin/nvbench-compare" --no-color "${NVBENCH_COMPARE_ARGS[@]}" "${base_json}" "${test_json}") + + : > "${compare_log}" + echo "::group::${label}" + print_shell_command "${compare_cmd[@]}" + started_at="${SECONDS}" + if "${compare_cmd[@]}" \ + > >(tee "${compare_out}" | tee -a "${compare_log}") \ + 2> >(tee -a "${compare_log}" >&2); then + rc=0 + else + rc=$? + fi + elapsed_s=$((SECONDS - started_at)) + echo "::endgroup::" + if [[ "${rc}" -eq 0 ]]; then + echo "${label} completed in ${elapsed_s}s" + else + echo "${label} failed in ${elapsed_s}s (rc=${rc})" + fi + return "${rc}" +} + +# ============================================================================ +# Common helpers +# ============================================================================ + +run_grouped_logged_command() { + local label="$1" + local log_path="$2" + shift 2 + local started_at=0 + local elapsed_s=0 + local rc=0 + local -a pipe_statuses + + echo "::group::${label}" + print_shell_command "$@" + started_at="${SECONDS}" + set +o pipefail + "$@" 2>&1 | tee "${log_path}" + pipe_statuses=("${PIPESTATUS[@]}") + set -o pipefail + if [[ "${pipe_statuses[0]}" -ne 0 ]]; then + rc="${pipe_statuses[0]}" + elif [[ "${pipe_statuses[1]}" -ne 0 ]]; then + rc="${pipe_statuses[1]}" + fi + elapsed_s=$((SECONDS - started_at)) + echo "::endgroup::" + if [[ "${rc}" -eq 0 ]]; then + echo "${label} completed in ${elapsed_s}s" + else + echo "${label} failed in ${elapsed_s}s (rc=${rc})" + fi + return "${rc}" +} + +run_compare_target() { + local target="$1" + local compare_script="$2" + local compare_script_dir="$3" + local base_json="$4" + local test_json="$5" + local compare_out="$6" + local compare_log="$7" + + local label="[compare] ${target}" + local started_at=0 + local elapsed_s=0 + local rc=0 + local compare_pythonpath="${compare_script_dir}${PYTHONPATH:+:${PYTHONPATH}}" + local -a compare_cmd + compare_cmd=(python3 "${compare_script}" --no-color "${NVBENCH_COMPARE_ARGS[@]}" "${base_json}" "${test_json}") + + : > "${compare_log}" + echo "::group::${label}" + print_shell_command --env "PYTHONPATH=${compare_pythonpath}" "${compare_cmd[@]}" + started_at="${SECONDS}" + if PYTHONPATH="${compare_pythonpath}" \ + "${compare_cmd[@]}" \ + > >(tee "${compare_out}" | tee -a "${compare_log}") \ + 2> >(tee -a "${compare_log}" >&2); then + rc=0 + else + rc=$? + fi + elapsed_s=$((SECONDS - started_at)) + echo "::endgroup::" + if [[ "${rc}" -eq 0 ]]; then + echo "${label} completed in ${elapsed_s}s" + else + echo "${label} failed in ${elapsed_s}s (rc=${rc})" + fi + return "${rc}" +} + +parse_quoted_args_to_nul_file() { + local quoted_args="$1" + local output_file="$2" + local option_name="$3" + + QUOTED_ARGS="${quoted_args}" OPTION_NAME="${option_name}" python3 - "${output_file}" <<'PY' +import os +import shlex +import sys + +quoted = os.environ.get("QUOTED_ARGS", "") +option_name = os.environ.get("OPTION_NAME", "--args") +try: + parsed = shlex.split(quoted) +except ValueError as exc: + print(f"Invalid {option_name}: {exc}", file=sys.stderr) + raise SystemExit(2) + +with open(sys.argv[1], "wb") as out_file: + for arg in parsed: + out_file.write(arg.encode("utf-8")) + out_file.write(b"\0") +PY +} + +parse_quoted_args_to_array() { + local -n _target_array_ref="$1" + local quoted_args="$2" + local option_label="$3" + local parsed_args_file="" + + _target_array_ref=() + [[ -n "${quoted_args}" ]] || return 0 + + parsed_args_file="$(mktemp "/tmp/cccl-parsed-args-XXXXXX")" + if ! parse_quoted_args_to_nul_file "${quoted_args}" "${parsed_args_file}" "${option_label}"; then + rm -f "${parsed_args_file}" + return 2 + fi + mapfile -d '' -t _target_array_ref < "${parsed_args_file}" + rm -f "${parsed_args_file}" +} + +# ============================================================================ +# Summary +# ============================================================================ + +write_summary() { + local summary_file="$1" + local target="" + local compare_report_file="" + local reports_emitted=0 + + { + echo "# Benchmark Comparison Summary" + echo + echo "- Timestamp (UTC): ${timestamp}" + echo "- GPU name: ${CCCL_BENCH_GPU_NAME:-not specified}" + echo "- Base label: ${base_label_raw}" + echo "- Test label: ${test_label_raw}" + echo "- Base source path: \`${BASE_PATH}\`" + echo "- Test source path: \`${TEST_PATH}\`" + if [[ "${#FILTERS[@]}" -gt 0 ]]; then + echo "- Base build dir: \`${base_build_dir}\`" + echo "- Test build dir: \`${test_build_dir}\`" + fi + echo "- CUB targets selected: ${#selected_targets[@]}" + echo "- CUB comparisons attempted: ${compares_attempted}" + echo "- CUB comparisons succeeded: ${compares_succeeded}" + echo "- Python targets selected: ${#selected_py_targets[@]}" + echo "- Python comparisons attempted: ${py_compares_attempted}" + echo "- Python comparisons succeeded: ${py_compares_succeeded}" + echo "- Target arch: ${TARGET_ARCH:-preset-default}" + echo "- Artifact directory: \`${artifact_dir}\`" + echo + + if [[ "${#FILTERS[@]}" -gt 0 ]]; then + echo "## CUB Filters" + for filter in "${FILTERS[@]}"; do + echo "- \`${filter}\`" + done + echo + fi + + if [[ "${#PYTHON_FILTERS[@]}" -gt 0 ]]; then + echo "## Python Filters" + for filter in "${PYTHON_FILTERS[@]}"; do + echo "- \`${filter}\`" + done + echo + fi + + if [[ "${#selected_targets[@]}" -gt 0 ]]; then + echo "## CUB Compare Reports" + for target in "${selected_targets[@]}"; do + compare_report_file="${artifact_dir}/compare/${target}.md" + if [[ ! -f "${compare_report_file}" ]]; then + continue + fi + reports_emitted=$((reports_emitted + 1)) + echo + echo "### \`${target}\`" + echo + echo "
Expand full compare output for \`${target}\`" + echo + cat "${compare_report_file}" + echo + echo "
" + done + fi + + if [[ "${#selected_py_targets[@]}" -gt 0 ]]; then + echo + echo "## Python Compare Reports" + local py_target_path="" + local py_target_name="" + for py_target_path in "${selected_py_targets[@]}"; do + py_target_name="$(python_path_to_target_name "${py_target_path}")" + compare_report_file="${artifact_dir}/compare/${py_target_name}.md" + if [[ ! -f "${compare_report_file}" ]]; then + continue + fi + reports_emitted=$((reports_emitted + 1)) + echo + echo "### \`${py_target_name}\` (\`${py_target_path}\`)" + echo + echo "
Expand full compare output for \`${py_target_name}\`" + echo + cat "${compare_report_file}" + echo + echo "
" + done + fi + + if [[ "${reports_emitted}" -eq 0 ]]; then + echo + echo "_No per-target compare reports were produced._" + fi + } > "${summary_file}" +} + +# ============================================================================ +# CLI parsing +# ============================================================================ + +parse_cli_args() { + if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 + fi + if [[ "$#" -lt 2 ]]; then + usage + exit 2 + fi + + BASE_PATH="$(realpath "$1")" + TEST_PATH="$(realpath "$2")" + shift 2 + + NVBENCH_ARGS_STRING="" + NVBENCH_COMPARE_ARGS_STRING="" + TARGET_ARCH="" + FILTERS=() + PYTHON_FILTERS=() + while [[ "$#" -gt 0 ]]; do + case "$1" in + --arch) + if [[ "$#" -lt 2 ]]; then + die "Missing value for --arch" + fi + TARGET_ARCH="$2" + shift 2 + ;; + --nvbench-args) + if [[ "$#" -lt 2 ]]; then + die "Missing value for --nvbench-args" + fi + NVBENCH_ARGS_STRING="$2" + shift 2 + ;; + --nvbench-compare-args) + if [[ "$#" -lt 2 ]]; then + die "Missing value for --nvbench-compare-args" + fi + NVBENCH_COMPARE_ARGS_STRING="$2" + shift 2 + ;; + --cub-filter) + if [[ "$#" -lt 2 ]]; then + die "Missing value for --cub-filter" + fi + FILTERS+=("$2") + shift 2 + ;; + --python-filter) + if [[ "$#" -lt 2 ]]; then + die "Missing value for --python-filter" + fi + PYTHON_FILTERS+=("$2") + shift 2 + ;; + --) + shift + break + ;; + *) + die "Unknown option: $1" + ;; + esac + done +} + +parse_cli_args "$@" + +declare -a NVBENCH_RUN_ARGS +declare -a NVBENCH_COMPARE_ARGS +parse_quoted_args_to_array NVBENCH_RUN_ARGS "${NVBENCH_ARGS_STRING}" "--nvbench-args" \ + || die "Failed to parse --nvbench-args." +parse_quoted_args_to_array NVBENCH_COMPARE_ARGS "${NVBENCH_COMPARE_ARGS_STRING}" "--nvbench-compare-args" \ + || die "Failed to parse --nvbench-compare-args." + +validate_repo_path "${BASE_PATH}" +validate_repo_path "${TEST_PATH}" +validate_filter_array FILTERS "CUB" +validate_filter_array PYTHON_FILTERS "Python" + +# ============================================================================ +# Common setup +# ============================================================================ + +timestamp="$(date -u +'%Y%m%dT%H%M%SZ')" +base_label_raw="${CCCL_BENCH_BASE_LABEL:-$(resolve_repo_label "${BASE_PATH}")}" +test_label_raw="${CCCL_BENCH_TEST_LABEL:-$(resolve_repo_label "${TEST_PATH}")}" +base_label="$(sanitize_label "${base_label_raw}")" +test_label="$(sanitize_label "${test_label_raw}")" + +artifact_root="${CCCL_BENCH_ARTIFACT_ROOT:-$(pwd)/bench-artifacts}" +gpu_tag="${CCCL_BENCH_GPU_NAME:+$(sanitize_label "${CCCL_BENCH_GPU_NAME}")-}" +artifact_tag="${CCCL_BENCH_ARTIFACT_TAG:-bench-${gpu_tag}${test_label}-${timestamp}-${base_label}}" +artifact_tag="$(sanitize_label "${artifact_tag}")" +artifact_dir="${artifact_root}/${artifact_tag}" + +build_root="${CCCL_BENCH_BUILD_ROOT:-/tmp/cccl-bench-builds}" +build_token="$(sanitize_label "${test_label}-${timestamp}-${base_label}")" +base_build_dir="${build_root}/base-${build_token}" +test_build_dir="${build_root}/test-${build_token}" + +for subdir in base compare logs meta test; do + mkdir -p "${artifact_dir}/${subdir}" +done +mkdir -p "${build_root}" + +echo "Artifact directory: ${artifact_dir}" +if [[ -n "${CCCL_BENCH_GPU_NAME:-}" ]]; then + echo "GPU name: ${CCCL_BENCH_GPU_NAME}" +fi +echo "Base source: ${BASE_PATH}" +echo "Test source: ${TEST_PATH}" +if [[ "${#FILTERS[@]}" -gt 0 ]]; then + echo "CUB filters:" + for filter in "${FILTERS[@]}"; do + echo " - ${filter}" + done +else + echo "CUB filters: (none)" +fi +if [[ "${#PYTHON_FILTERS[@]}" -gt 0 ]]; then + echo "Python filters:" + for filter in "${PYTHON_FILTERS[@]}"; do + echo " - ${filter}" + done +else + echo "Python filters: (none)" +fi +if [[ -n "${TARGET_ARCH}" ]]; then + echo "Target arch: ${TARGET_ARCH}" +fi +if [[ "${#NVBENCH_RUN_ARGS[@]}" -gt 0 ]]; then + echo "Extra run args:" + for arg in "${NVBENCH_RUN_ARGS[@]}"; do + echo " - ${arg}" + done +fi +if [[ "${#NVBENCH_COMPARE_ARGS[@]}" -gt 0 ]]; then + echo "Extra compare args:" + for arg in "${NVBENCH_COMPARE_ARGS[@]}"; do + echo " - ${arg}" + done +fi + +any_failures=0 +compares_attempted=0 +compares_succeeded=0 +declare -a selected_targets=() +py_compares_attempted=0 +py_compares_succeeded=0 +declare -a selected_py_targets=() + +# ============================================================================ +# CUB benchmark pipeline +# ============================================================================ + +if [[ "${#FILTERS[@]}" -gt 0 ]]; then + echo + echo "=== CUB Benchmark Pipeline ===" + echo + + external_base_build_dir="${CCCL_BENCH_BASE_BUILD_DIR:-}" + external_test_build_dir="${CCCL_BENCH_TEST_BUILD_DIR:-}" + if [[ -n "${external_base_build_dir}" || -n "${external_test_build_dir}" ]]; then + if [[ -z "${external_base_build_dir}" || -z "${external_test_build_dir}" ]]; then + die "Both CCCL_BENCH_BASE_BUILD_DIR and CCCL_BENCH_TEST_BUILD_DIR must be set together." + fi + base_build_dir="$(realpath "${external_base_build_dir}")" + test_build_dir="$(realpath "${external_test_build_dir}")" + validate_build_dir "${base_build_dir}" "base" + validate_build_dir "${test_build_dir}" "test" + if [[ -n "${TARGET_ARCH}" ]]; then + echo "Warning: --arch is ignored when using preconfigured build directories." >&2 + fi + fi + + if [[ -z "${external_base_build_dir:-}" ]]; then + configure_build_tree "${BASE_PATH}" "${base_build_dir}" "base" "${artifact_dir}/logs/configure.base.log" "${TARGET_ARCH}" + configure_build_tree "${TEST_PATH}" "${test_build_dir}" "test" "${artifact_dir}/logs/configure.test.log" "${TARGET_ARCH}" + else + echo "[configure:base] skipped (using existing build tree)" + echo "[configure:test] skipped (using existing build tree)" + fi + + select_targets "${base_build_dir}" "${test_build_dir}" selected_targets + + printf "%s\n" "${selected_targets[@]}" > "${artifact_dir}/meta/selected_targets.txt" + + compare_script="$(resolve_compare_script "${test_build_dir}" || true)" + if [[ -z "${compare_script}" ]]; then + compare_script="$(resolve_compare_script "${base_build_dir}" || true)" + fi + if [[ -z "${compare_script}" ]]; then + die "Unable to locate nvbench_compare.py in build dependencies." 1 + fi + compare_script_dir="$(dirname "${compare_script}")" + + base_build_all_rc=0 + test_build_all_rc=0 + + if run_grouped_logged_command \ + "[build:base]" \ + "${artifact_dir}/logs/build.base.log" \ + ninja -C "${base_build_dir}" "${selected_targets[@]}"; then + base_build_all_rc=0 + else + base_build_all_rc=$? + any_failures=1 + fi + + if run_grouped_logged_command \ + "[build:test]" \ + "${artifact_dir}/logs/build.test.log" \ + ninja -C "${test_build_dir}" "${selected_targets[@]}"; then + test_build_all_rc=0 + else + test_build_all_rc=$? + any_failures=1 + fi + + for target in "${selected_targets[@]}"; do + base_target_run_rc=125 + test_target_run_rc=125 + base_run_log="${artifact_dir}/logs/run.base.${target}.log" + test_run_log="${artifact_dir}/logs/run.test.${target}.log" + compare_report_md="${artifact_dir}/compare/${target}.md" + compare_report_log="${artifact_dir}/logs/compare.${target}.log" + + base_json="${artifact_dir}/base/${target}.json" + base_md="${artifact_dir}/base/${target}.md" + test_json="${artifact_dir}/test/${target}.json" + test_md="${artifact_dir}/test/${target}.md" + + if [[ "${base_build_all_rc}" -eq 0 ]]; then + if run_target_for_side \ + "base" \ + "${base_build_dir}" \ + "${target}" \ + "${base_json}" \ + "${base_md}" \ + "${base_run_log}"; then + base_target_run_rc=0 + else + base_target_run_rc=$? + any_failures=1 + fi + fi + + if [[ "${test_build_all_rc}" -eq 0 ]]; then + if run_target_for_side \ + "test" \ + "${test_build_dir}" \ + "${target}" \ + "${test_json}" \ + "${test_md}" \ + "${test_run_log}"; then + test_target_run_rc=0 + else + test_target_run_rc=$? + any_failures=1 + fi + fi + + if [[ "${base_target_run_rc}" -eq 0 && "${test_target_run_rc}" -eq 0 ]]; then + compares_attempted=$((compares_attempted + 1)) + if run_compare_target \ + "${target}" \ + "${compare_script}" \ + "${compare_script_dir}" \ + "${base_json}" \ + "${test_json}" \ + "${compare_report_md}" \ + "${compare_report_log}"; then + compares_succeeded=$((compares_succeeded + 1)) + else + any_failures=1 + fi + fi + done +fi + +# ============================================================================ +# Python benchmark pipeline +# ============================================================================ + +if [[ "${#PYTHON_FILTERS[@]}" -gt 0 ]]; then + echo + echo "=== Python Benchmark Pipeline ===" + echo + + py_benchmarks_subdir="python/cuda_cccl/benchmarks" + base_py_bench_dir="${BASE_PATH}/${py_benchmarks_subdir}" + test_py_bench_dir="${TEST_PATH}/${py_benchmarks_subdir}" + + if [[ ! -d "${base_py_bench_dir}" ]]; then + die "Python benchmarks directory not found in base tree: ${base_py_bench_dir}" + fi + if [[ ! -d "${test_py_bench_dir}" ]]; then + die "Python benchmarks directory not found in test tree: ${test_py_bench_dir}" + fi + + cuda_major="$(detect_cuda_major_version)" + echo "Detected CUDA major version: ${cuda_major}" + + base_py_venv="${build_root}/py-base-${build_token}" + test_py_venv="${build_root}/py-test-${build_token}" + + setup_python_venv "${base_py_venv}" "${BASE_PATH}" "base" "${artifact_dir}/logs/py.venv.base.log" "${cuda_major}" + setup_python_venv "${test_py_venv}" "${TEST_PATH}" "test" "${artifact_dir}/logs/py.venv.test.log" "${cuda_major}" + + select_python_targets "${base_py_bench_dir}" "${test_py_bench_dir}" selected_py_targets + + # Append Python targets to the selected targets metadata file. + for py_target_path in "${selected_py_targets[@]}"; do + python_path_to_target_name "${py_target_path}" >> "${artifact_dir}/meta/selected_targets.txt" + done + + for py_target_path in "${selected_py_targets[@]}"; do + py_target_name="$(python_path_to_target_name "${py_target_path}")" + base_py_target_run_rc=125 + test_py_target_run_rc=125 + + base_py_json="${artifact_dir}/base/${py_target_name}.json" + base_py_md="${artifact_dir}/base/${py_target_name}.md" + test_py_json="${artifact_dir}/test/${py_target_name}.json" + test_py_md="${artifact_dir}/test/${py_target_name}.md" + base_py_run_log="${artifact_dir}/logs/run.base.${py_target_name}.log" + test_py_run_log="${artifact_dir}/logs/run.test.${py_target_name}.log" + compare_py_report_md="${artifact_dir}/compare/${py_target_name}.md" + compare_py_report_log="${artifact_dir}/logs/compare.${py_target_name}.log" + + if run_python_target_for_side \ + "base" \ + "${base_py_venv}" \ + "${base_py_bench_dir}/${py_target_path}" \ + "${base_py_json}" \ + "${base_py_md}" \ + "${base_py_run_log}"; then + base_py_target_run_rc=0 + else + base_py_target_run_rc=$? + any_failures=1 + fi + + if run_python_target_for_side \ + "test" \ + "${test_py_venv}" \ + "${test_py_bench_dir}/${py_target_path}" \ + "${test_py_json}" \ + "${test_py_md}" \ + "${test_py_run_log}"; then + test_py_target_run_rc=0 + else + test_py_target_run_rc=$? + any_failures=1 + fi + + if [[ "${base_py_target_run_rc}" -eq 0 && "${test_py_target_run_rc}" -eq 0 ]]; then + py_compares_attempted=$((py_compares_attempted + 1)) + if run_python_compare_target \ + "${py_target_name}" \ + "${test_py_venv}" \ + "${base_py_json}" \ + "${test_py_json}" \ + "${compare_py_report_md}" \ + "${compare_py_report_log}"; then + py_compares_succeeded=$((py_compares_succeeded + 1)) + else + any_failures=1 + fi + fi + done +fi + +# ============================================================================ +# Summary and exit +# ============================================================================ + +summary_file="${artifact_dir}/summary.md" +write_summary "${summary_file}" + +echo "Wrote summary: ${summary_file}" +echo "Benchmark artifacts: ${artifact_dir}" +echo +echo "Main summary:" +cat "${summary_file}" +echo + +if [[ "${any_failures}" -ne 0 ]]; then + exit 1 +fi + +exit 0 diff --git a/cccl_upstream/ci/bench/parse_bench_matrix.sh b/cccl_upstream/ci/bench/parse_bench_matrix.sh new file mode 100755 index 00000000..76ce2540 --- /dev/null +++ b/cccl_upstream/ci/bench/parse_bench_matrix.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -euo pipefail + +die() { + local message="$1" + local code="${2:-2}" + echo "${message}" >&2 + exit "${code}" +} + +usage() { + cat </dev/null 2>&1 || die "'yq' is required to parse ${bench_yaml_path}." +command -v "jq" >/dev/null 2>&1 || die "'jq' is required to build the dispatch matrix." + +if ! bench_cfg_json="$(yq -o=json '.benchmarks // {}' "${bench_yaml_path}" 2>&1)"; then + die "Failed to parse ${bench_yaml_path} as YAML: ${bench_cfg_json}" +fi + +# Extract CUB and Python filter arrays (default to empty arrays). +cub_filters_json="$(jq -c '.filters.cub // []' <<<"${bench_cfg_json}")" +python_filters_json="$(jq -c '.filters.python // []' <<<"${bench_cfg_json}")" + +has_cub_filters="$(jq -e 'type == "array" and length > 0 and all(.[]; type == "string")' <<<"${cub_filters_json}" >/dev/null 2>&1 && echo true || echo false)" +has_python_filters="$(jq -e 'type == "array" and length > 0 and all(.[]; type == "string")' <<<"${python_filters_json}" >/dev/null 2>&1 && echo true || echo false)" + +if [[ "${has_cub_filters}" != "true" && "${has_python_filters}" != "true" ]]; then + die "${bench_yaml_path} must define at least one string entry in benchmarks.filters.cub or benchmarks.filters.python." +fi + +cub_filters_arg="" +if [[ "${has_cub_filters}" == "true" ]]; then + cub_filters_arg="$(jq -r '.filters.cub | map(@sh) | join(" ")' <<<"${bench_cfg_json}")" +fi + +python_filters_arg="" +if [[ "${has_python_filters}" == "true" ]]; then + python_filters_arg="$(jq -r '.filters.python | map(@sh) | join(" ")' <<<"${bench_cfg_json}")" +fi + +jq -cn \ + --argjson cfg "${bench_cfg_json}" \ + --arg cub_filters "${cub_filters_arg}" \ + --arg python_filters "${python_filters_arg}" \ + '{ + "include": [ + ($cfg.gpus // [])[] as $gpu + | { + "gpu": $gpu, + "launch_args": ($cfg.launch_args // ""), + "arch": ($cfg.arch // "native"), + "base_ref": ($cfg.base_ref // "origin/main"), + "test_ref": ($cfg.test_ref // "HEAD"), + "cub_filters": $cub_filters, + "python_filters": $python_filters, + "nvbench_args": ($cfg.nvbench_args // ""), + "nvbench_compare_args": ($cfg.nvbench_compare_args // "") + } + ] + }' diff --git a/cccl_upstream/ci/build_cccl_c_parallel.sh b/cccl_upstream/ci/build_cccl_c_parallel.sh new file mode 100755 index 00000000..4ebdacec --- /dev/null +++ b/cccl_upstream/ci/build_cccl_c_parallel.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +PRESET="cccl-c-parallel" + +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") + +configure_and_build_preset "CCCL C Parallel Library" "$PRESET" "${CMAKE_OPTIONS[@]}" + +print_time_summary diff --git a/cccl_upstream/ci/build_cccl_c_stf.sh b/cccl_upstream/ci/build_cccl_c_stf.sh new file mode 100755 index 00000000..5775ca7a --- /dev/null +++ b/cccl_upstream/ci/build_cccl_c_stf.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +PRESET="cccl-c-stf" + +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") + +configure_and_build_preset "CCCL C CUDASTF Library" "$PRESET" "${CMAKE_OPTIONS[@]}" + +print_time_summary diff --git a/cccl_upstream/ci/build_common.sh b/cccl_upstream/ci/build_common.sh new file mode 100755 index 00000000..43df0934 --- /dev/null +++ b/cccl_upstream/ci/build_common.sh @@ -0,0 +1,501 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "This script must be sourced, not executed directly." >&2 + exit 1 +fi + +# Ensure the script is being executed in its containing directory +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +# Script defaults +VERBOSE=${VERBOSE:-} +HOST_COMPILER=${CXX:-g++} # $CXX if set, otherwise `g++` +CXX_STANDARD=17 +CUDA_COMPILER=${CUDACXX:-nvcc} # $CUDACXX if set, otherwise `nvcc` +CUDA_ARCHS= # Empty, use presets by default. +GLOBAL_CMAKE_OPTIONS=() +DISABLE_CUB_BENCHMARKS= # Enable to force-disable building CUB benchmarks. +PEDANTIC=${PEDANTIC:-} # Enable strict warnings. Default: on in CI, off locally. +CONFIGURE_ONLY=false +CTEST_PARALLEL_LEVEL=1 + +# Check if the correct number of arguments has been provided +function usage { + echo "Usage: $0 [OPTIONS]" + echo + echo "The PARALLEL_LEVEL environment variable controls the amount of build parallelism. Default is the number of cores minus one." + echo + echo "Options:" + echo " -v/-verbose: enable shell echo for debugging" + echo " -configure: Only run cmake to configure, do not build or test." + echo " -cuda: CUDA compiler (Defaults to \$CUDACXX if set, otherwise nvcc)" + echo " -cxx: Host compiler (Defaults to \$CXX if set, otherwise g++)" + echo " -std: CUDA/C++ standard (Defaults to 17)" + echo " -arch: Target CUDA arches, e.g. \"60-real;70;80-virtual\" (Defaults to value in presets file)" + echo " --test-par: CTest parallel level (Defaults to 1)" + echo " -pedantic/--pedantic: Enable strict warnings-as-errors and expose CCCL header warnings (default in CI)" + echo " -cmake-options: Additional options to pass to CMake" + echo + echo "Examples:" + echo " $ PARALLEL_LEVEL=8 $0" + echo " $ PARALLEL_LEVEL=8 $0 -cxx g++-9" + echo " $ $0 -cxx clang++-8" + echo " $ $0 -configure -arch 80" + echo " $ $0 -cxx g++-8 -std 14 -arch 80-real -v -cuda /usr/local/bin/nvcc" + echo " $ $0 -cmake-options \"-DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=-Wfatal-errors\"" + exit 1 +} + +# Check for required dependencies +function check_required_dependencies() { + local missing_deps=() + + # Check for essential tools + local required_tools=("cmake" "git" "jq" "ninja" "nproc") + for tool in "${required_tools[@]}"; do + command -v "$tool" &>/dev/null || missing_deps+=("$tool") + done + + if [[ ${#missing_deps[@]} -ne 0 ]]; then + echo "❌ Error: Missing required dependencies:" >&2 + printf " • %s\n" "${missing_deps[@]}" >&2 + echo >&2 + exit 1 + fi +} + +# Parse options + +# Copy the args into a temporary array, since we will modify them and +# the parent script may still need them. +args=("$@") +while [[ "${#args[@]}" -ne 0 ]]; do + case "${args[0]}" in + -v | --verbose | -verbose) VERBOSE=1; args=("${args[@]:1}");; + -configure) CONFIGURE_ONLY=true; args=("${args[@]:1}");; + -cxx) HOST_COMPILER="${args[1]}"; args=("${args[@]:2}");; + -std) CXX_STANDARD="${args[1]}"; args=("${args[@]:2}");; + -cuda) CUDA_COMPILER="${args[1]}"; args=("${args[@]:2}");; + -arch) CUDA_ARCHS="${args[1]}"; args=("${args[@]:2}");; + --test-par) CTEST_PARALLEL_LEVEL="${args[1]}"; args=("${args[@]:2}");; + -pedantic | --pedantic) PEDANTIC=1; args=("${args[@]:1}");; + -disable-benchmarks) export DISABLE_CUB_BENCHMARKS=1; args=("${args[@]:1}");; + -cmake-options) + if [[ -n "${args[1]}" ]]; then + IFS=' ' read -ra split_args <<< "${args[1]}" + GLOBAL_CMAKE_OPTIONS+=("${split_args[@]}") + args=("${args[@]:2}") + else + echo "Error: No arguments provided for -cmake-options" + usage + # usage will exit 1 for us, so below exit 1 is unreachable, but it does not + # hurt and guards against changes in usage. + # shellcheck disable=SC2317 + exit 1 + fi + ;; + -h | -help | --help) usage ;; + *) echo "Unrecognized option: ${args[0]}"; usage ;; + esac +done + +# Convert to full paths and validate compilers exist: +function validate_and_resolve_compiler() { + local compiler_name="$1" + local compiler_var="$2" + local compiler_path + + compiler_path=$(command -v "${compiler_var}" 2>/dev/null) + if [[ -z "$compiler_path" ]]; then + echo "❌ Error: ${compiler_name} '${compiler_var}' not found in PATH" >&2 + exit 1 + fi + + echo "$compiler_path" +} + +HOST_COMPILER=$(validate_and_resolve_compiler "Host compiler" "${HOST_COMPILER}") +CUDA_COMPILER=$(validate_and_resolve_compiler "CUDA compiler" "${CUDA_COMPILER}") + +if [[ "$(basename "$CUDA_COMPILER")" == nvcc* ]]; then + NVCC_VERSION=$("$CUDA_COMPILER" --version | grep "release" | sed 's/.*, V//') + # Verify that we have an X.Y.Z version in case the output format changes: + if ! [[ "$NVCC_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Error: Detected nvcc version is not a valid X.Y.Z triple: '$NVCC_VERSION'" >&2 + echo "$CUDA_COMPILER --version" >&2 || : + $CUDA_COMPILER --version >&2 || : + exit 1 + fi +fi + + +if [[ -n "${CUDA_ARCHS}" ]]; then + GLOBAL_CMAKE_OPTIONS+=("-DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}") +fi + +# Default to pedantic mode in CI +if [[ -z "${PEDANTIC}" && -n "${GITHUB_ACTIONS:-}" ]]; then + PEDANTIC=1 +fi + +if [[ -n "${PEDANTIC}" ]]; then + GLOBAL_CMAKE_OPTIONS+=("-DCCCL_ENABLE_WERROR=ON" "-DCCCL_ENABLE_PRAGMA_SYSTEM_HEADER=OFF") +else + GLOBAL_CMAKE_OPTIONS+=("-DCCCL_ENABLE_WERROR=OFF" "-DCCCL_ENABLE_PRAGMA_SYSTEM_HEADER=ON") +fi + +if [[ -n "$VERBOSE" ]]; then + set -x +fi + +# Check for required dependencies +check_required_dependencies + +# Begin processing unsets after option parsing +set -u + +N_CPUS="$(nproc --all --ignore=1)" +readonly N_CPUS +readonly PARALLEL_LEVEL="${PARALLEL_LEVEL:=${N_CPUS}}" + +if [[ -z ${CCCL_BUILD_INFIX+x} ]]; then + CCCL_BUILD_INFIX="" +fi + +mkdir -p ../build +# Absolute path to cccl/build +BUILD_ROOT=$(cd "../build" && pwd) + +# Absolute path to per-devcontainer build directory +BUILD_DIR="$BUILD_ROOT/$CCCL_BUILD_INFIX" + +# The most recent devcontainer build dir will always be symlinked to cccl/build/latest +mkdir -p "$BUILD_DIR" +rm -f "$BUILD_ROOT"/latest +ln -sf "$BUILD_DIR" "$BUILD_ROOT"/latest + +# The more recent preset build dir will always be symlinked to: +# cccl/preset-latest +function symlink_latest_preset { + local PRESET=$1 + mkdir -p "$BUILD_DIR/$PRESET" + rm -f "$BUILD_ROOT/preset-latest" + ln -sf "$BUILD_DIR/$PRESET" "$BUILD_ROOT/preset-latest" +} + +# Now that BUILD_DIR exists, use readlink to canonicalize the path: +BUILD_DIR=$(readlink -f "${BUILD_DIR}") + +# Prepare environment for CMake: +export CMAKE_BUILD_PARALLEL_LEVEL="$((PARALLEL_LEVEL > N_CPUS ? N_CPUS : PARALLEL_LEVEL))" + +export CTEST_PARALLEL_LEVEL +export CXX="${HOST_COMPILER}" +export CUDACXX="${CUDA_COMPILER}" +export CUDAHOSTCXX="${HOST_COMPILER}" +export CXX_STANDARD + +# shellcheck source=ci/pretty_printing.sh +source ./pretty_printing.sh + +# Kill any build / test steps that exceed this time, otherwise CI jobs may be +# killed by GHA before they can upload logs / artifacts needed to reproduce the timeout. +# Only applies when running inside GitHub Actions. +# Note that this is per-build/test limit, not a total timeout for the entire job. +: "${CCCL_CI_COMMAND_TIMEOUT:=5.5h}" + +print_environment_details() { + begin_group "⚙️ Environment Details" + + echo "free -h:" + free -h || : + + echo "nproc=$(nproc || :)" + + echo "pwd=$(pwd)" + + print_var_values \ + BUILD_DIR \ + CXX_STANDARD \ + CXX \ + CUDACXX \ + CUDAHOSTCXX \ + NVCC_VERSION \ + CMAKE_BUILD_PARALLEL_LEVEL \ + CTEST_PARALLEL_LEVEL \ + CCCL_CI_COMMAND_TIMEOUT \ + CCCL_CUDA_EXTENDED \ + CCCL_BUILD_INFIX \ + PEDANTIC \ + GLOBAL_CMAKE_OPTIONS \ + TBB_ROOT + + echo "Current commit is:" + git log -1 --format=short || echo "Not a repository" + + if command -v nvidia-smi &> /dev/null; then + nvidia-smi + else + echo "nvidia-smi not found" + fi + + if command -v sccache &> /dev/null; then + sccache --version + else + echo "sccache not found" + fi + + if command -v cmake &> /dev/null; then + cmake --version + else + echo "cmake not found" + fi + + if command -v ctest &> /dev/null; then + ctest --version + else + echo "ctest not found" + fi + + end_group "⚙️ Environment Details" +} + +run_ci_timed_command() { + local group_name="${1:-}" + shift + local -a command=("$@") + + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + if [[ -n "${CCCL_CI_COMMAND_TIMEOUT}" && "${CCCL_CI_COMMAND_TIMEOUT}" != "0" ]]; then + if command -v timeout &> /dev/null; then + run_command "${group_name}" timeout "${CCCL_CI_COMMAND_TIMEOUT}" "${command[@]}" + return $? + fi + echo "Warning: timeout not found; running without CI timeout." >&2 + fi + fi + + run_command "${group_name}" "${command[@]}" +} + +fail_if_no_gpu() { + if ! nvidia-smi &> /dev/null; then + echo "Error: No NVIDIA GPU detected. Please ensure you have an NVIDIA GPU installed and the drivers are properly configured." >&2 + exit 1 + fi +} + +function cccl_configure_preset_for_test() { + local test_preset=$1 + local presets_file="${BUILD_ROOT}/../CMakePresets.json" + local configure_preset + + if [[ ! -f "${presets_file}" ]]; then + echo "Error: CMakePresets.json not found: ${presets_file}" >&2 + return 1 + fi + + configure_preset=$( + jq -r --arg t "${test_preset}" ' + .testPresets[] | select(.name == $t) | .configurePreset // empty + ' "${presets_file}" + ) + + if [[ -z "${configure_preset}" ]]; then + configure_preset="${test_preset}" + fi + + echo "${configure_preset}" +} + +function cccl_smoke_tests_enabled() { + local configure_preset=$1 + local cache_file="${BUILD_DIR}/${configure_preset}/CMakeCache.txt" + + [[ -f "${cache_file}" ]] \ + && grep -q '^CCCL_ENABLE_CUDA_SMOKE_TESTS:BOOL=ON' "${cache_file}" +} + +function run_cuda_smoke_test() { + local BUILD_NAME=$1 + local test_preset=$2 + + local configure_preset + configure_preset="$(cccl_configure_preset_for_test "${test_preset}")" + local smoke_bin="${BUILD_DIR}/${configure_preset}/bin/cccl.test.cuda_runtime_smoke" + + if [[ -x "${smoke_bin}" ]]; then + run_ci_timed_command "CUDA smoke ${BUILD_NAME}" "${smoke_bin}" || return $? + elif cccl_smoke_tests_enabled "${configure_preset}"; then + echo "Error: CCCL_ENABLE_CUDA_SMOKE_TESTS=ON but smoke binary not found: ${smoke_bin}" >&2 + return 1 + fi +} + +function print_test_time_summary() +{ + ctest_log=${1} + + if [[ -f "${ctest_log}" ]]; then + begin_group "⏱️ Longest Test Steps" + # Only print the full output in CI: + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + cmake -DLOGFILE="${ctest_log}" -P ../cmake/PrintCTestRunTimes.cmake + else + # `|| :` to avoid `set -o pipefail` from triggering when `head` closes the pipe before `cmake` finishes. + # Otherwise the script will exit early with status 141 (SIGPIPE). + cmake -DLOGFILE="${ctest_log}" -P ../cmake/PrintCTestRunTimes.cmake | head -n 15 || : + fi + end_group "⏱️ Longest Test Steps" + fi +} + +function configure_preset() +{ + local BUILD_NAME=$1 + local PRESET=$2 + shift 2 + local CMAKE_OPTIONS=("$@") + + local GROUP_NAME="🛠️ CMake Configure ${BUILD_NAME}" + + symlink_latest_preset "$PRESET" + + pushd .. > /dev/null + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + # Retry 5 times with 30 seconds between attempts to try to WAR network issues during CPM fetch on CI runners: + export RUN_COMMAND_RETRY_PARAMS=(5 30) + fi + status=0 + SCCACHE_NO_DIST_COMPILE=1 run_command "$GROUP_NAME" cmake --preset="$PRESET" --log-level=VERBOSE "${CMAKE_OPTIONS[@]}" "${GLOBAL_CMAKE_OPTIONS[@]}" || status=$? + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + unset RUN_COMMAND_RETRY_PARAMS + fi + popd > /dev/null + + if $CONFIGURE_ONLY; then + echo "${BUILD_NAME} configuration complete:" + echo " Exit code: ${status}" + echo " CMake Preset: ${PRESET}" + echo " CMake Options: ${CMAKE_OPTIONS[*]}" + echo " Build Directory: ${BUILD_DIR}/${PRESET}" + exit "$status" + fi + + return "$status" +} + +function build_preset() { + local BUILD_NAME=$1 + local PRESET=$2 + # shellcheck disable=SC2034 + local green="1;32" + # shellcheck disable=SC2034 + local red="1;31" + local GROUP_NAME="🏗️ Build ${BUILD_NAME}" + shift 2 + local BUILD_COMMANDS=("$@") + + symlink_latest_preset "$PRESET" + + if $CONFIGURE_ONLY; then + return 0 + fi + + local preset_dir="${BUILD_DIR}/${PRESET}" + local sccache_json="${preset_dir}/sccache_stats.json" + local memmon_log="${preset_dir}/memmon.log" + + sccache -z > /dev/null || : + + # Track memory usage on CI: + if [[ -n "${GITHUB_ACTIONS:-}" || -n "${MEMMON:-}" ]]; then + util/memmon.sh --start \ + --log-threshold "${MEMMON_LOG_THRESHOLD:-2}" \ + --print-threshold "${MEMMON_PRINT_THRESHOLD:-5}" \ + --log-file "$memmon_log" \ + --poll "${MEMMON_POLL_INTERVAL:-5}" \ + || : + fi + + pushd .. > /dev/null + status=0 + run_ci_timed_command "$GROUP_NAME" cmake --build --parallel "$PARALLEL_LEVEL" --preset="$PRESET" ${VERBOSE:+-v} "${BUILD_COMMANDS[@]}" || status=$? + popd > /dev/null + + if [[ -n "${GITHUB_ACTIONS:-}" || -n "${MEMMON:-}" ]]; then + util/memmon.sh --stop || : + run_command "📝 Memory Monitor Log" head -n20 "$memmon_log" || : + fi + + # Only print detailed stats in actions workflow + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + sccache --show-adv-stats --stats-format=json > "${sccache_json}" || : + run_command "📊 sccache stats" sccache --show-adv-stats || : + + begin_group "🥷 ninja build times" + echo "The \"weighted\" time is the elapsed time of each build step divided by the number + of tasks that were running in parallel. This makes it an excellent approximation + of how \"important\" a slow step was. A link that is entirely or mostly serialized + will have a weighted time that is the same or similar to its elapsed time. A + compile that runs in parallel with 999 other compiles will have a weighted time + that is tiny." + ./ninja_summary.py -C "${BUILD_DIR}"/"${PRESET}" || echo "Warning: ninja_summary.py failed to execute properly." + end_group + else + sccache -s || : + fi + + return "$status" +} + +function test_preset() +{ + local BUILD_NAME=$1 + local PRESET=$2 + local GPU_REQUIRED=${3:-true} + + symlink_latest_preset "$PRESET" + + if $CONFIGURE_ONLY; then + return 0 + fi + + if $GPU_REQUIRED; then + fail_if_no_gpu + if [[ -z "${CCCL_SKIP_CI_SMOKE:-}" ]]; then + run_cuda_smoke_test "${BUILD_NAME}" "${PRESET}" || return $? + fi + fi + + local GROUP_NAME="🚀 Test ${BUILD_NAME}" + + local preset_dir="${BUILD_DIR}/${PRESET}" + local ctest_log="${preset_dir}/ctest.log" + + pushd .. > /dev/null + status=0 + run_ci_timed_command "$GROUP_NAME" ctest --output-on-failure --output-log "${ctest_log}" --preset="$PRESET" || status=$? + popd > /dev/null + + print_test_time_summary "${ctest_log}" + + return "$status" +} + +function configure_and_build_preset() +{ + local BUILD_NAME=$1 + local PRESET=$2 + shift 2 + local CMAKE_OPTIONS=("$@") + + configure_preset "$BUILD_NAME" "$PRESET" "${CMAKE_OPTIONS[@]}" + + if ! $CONFIGURE_ONLY; then + build_preset "$BUILD_NAME" "$PRESET" + fi +} diff --git a/cccl_upstream/ci/build_compile_time_bench.sh b/cccl_upstream/ci/build_compile_time_bench.sh new file mode 100755 index 00000000..c90b0215 --- /dev/null +++ b/cccl_upstream/ci/build_compile_time_bench.sh @@ -0,0 +1,370 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${ci_dir}/.." && pwd)" +tool_dir="${repo_root}/ci/compile_time" + +default_preset="all-dev" +preset="${default_preset}" +skip_configure=0 +skip_build=0 +prepare_perfetto=1 +run_ctadvisor=0 +write_tu_csv=1 +explicit_tu_csv=0 +tu_csv="" +perfetto_output_dir="" +baseline_ref="" +baseline_worktree="" +max_detail_len=180 +cloc_processes=0 + +declare -a build_targets=() +declare -a event_args=() +declare -a common_args=() +declare -a default_build_targets=( + "cub.headers.base" + "thrust.cpp.cuda.headers.base" + "libcudacxx.test.public_headers" +) +declare -a bench_cmake_args=( + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" + "-DCMAKE_CUDA_COMPILER_LAUNCHER=" + "-DCMAKE_CXX_COMPILER_LAUNCHER=" + "-DCCCL_ENABLE_TESTING=OFF" + "-DCCCL_ENABLE_EXAMPLES=OFF" + "-DCCCL_ENABLE_BENCHMARKS=OFF" + "-DCCCL_ENABLE_C_PARALLEL=OFF" + "-DCCCL_ENABLE_C_EXPERIMENTAL_STF=OFF" + "-DCUB_ENABLE_TESTING=OFF" + "-DCUB_ENABLE_EXAMPLES=OFF" + "-DTHRUST_ENABLE_TESTING=OFF" + "-DTHRUST_ENABLE_EXAMPLES=OFF" + "-DTHRUST_MULTICONFIG_WORKLOAD=SMALL" + "-DTHRUST_MULTICONFIG_ENABLE_SYSTEM_OMP=OFF" + "-DTHRUST_MULTICONFIG_ENABLE_SYSTEM_TBB=OFF" + "-Dcudax_ENABLE_TESTING=OFF" + "-Dcudax_ENABLE_EXAMPLES=OFF" + "-Dcudax_ENABLE_CUDASTF=OFF" + "-Dcudax_ENABLE_CUFILE=OFF" + "-DCCCL_COMPILE_TIME_SAVE_PREPROCESSED_TUS=ON" + "-DCCCL_COMPILE_TIME_GENERATE_DEVICE_TIME_TRACES=ON" +) + +usage() { + cat <<'EOF' +Usage: ci/build_compile_time_bench.sh [options] [-- ] + +Build options: + -preset CMake configure preset (default: all-dev) + -cmake-options Extra CMake configure options handled by ci/build_common.sh + -target Build target; repeatable + (default: public include-check target set) + -baseline-ref Build this commit-ish as a comparison baseline + -skip-configure Do not run cmake configure + -skip-build Do not run cmake --build + -cuda, -cxx, -std, -arch Common compiler/standard/arch options from ci/build_common.sh + +Summary options: + -tu-csv Generated-TU summary CSV + (default: /compile_time/tu_summary.csv) + -no-tu-csv Do not write the generated-TU summary CSV + -prepare-perfetto Prepare Perfetto-friendly trace copies (default) + -no-prepare-perfetto Skip Perfetto trace preparation + -perfetto-output Perfetto trace output directory + (default: /compile_time/perfetto_traces) + -max-detail-len Max promoted detail length for Perfetto traces (default: 180) + -cloc-processes cloc process count for generated-TU summary CSV + -ctadvisor Print ctadvisor report for raw traces + +Event summary args: + Arguments after '--' are forwarded to ci/compile_time/summarize_events.py + after the raw trace directory. If omitted, the default summary is: + -f file-processing -e -n 15 + Pass --slices after '--' to emit multiple event report slices + and an event_reports/summary.json manifest. + With -baseline-ref, comparison-only options such as --threshold + may also be passed after '--'. Baseline raw traces are preserved under + /compile_time/baseline_raw_traces. + +Examples: + ci/build_compile_time_bench.sh + ci/build_compile_time_bench.sh -target cudax.headers.basic.no_stf -- -f scanning-function-body -i -n 20 + ci/build_compile_time_bench.sh -preset cub -target cub.headers.base -- -f template-instantiation -e -n 15 + ci/build_compile_time_bench.sh -baseline-ref origin/main -- -f file-processing -e --threshold 0.001 +EOF +} + +status() { echo "[compile-time-bench] $*" >&2; } + +require_command() { + local command_name="$1" + command -v "${command_name}" >/dev/null \ + || { echo "error: ${command_name} not found" >&2; exit 1; } +} + +cleanup_baseline_worktree() { + if [[ -n "${baseline_worktree}" && -d "${baseline_worktree}" ]]; then + git -C "${repo_root}" worktree remove --force "${baseline_worktree}" >/dev/null 2>&1 \ + || rm -rf "${baseline_worktree}" + fi +} + +cleanup_baseline_worktree_and_exit() { + local exit_code="$1" + trap - EXIT HUP INT TERM + cleanup_baseline_worktree + exit "${exit_code}" +} + +install_baseline_cleanup_traps() { + trap cleanup_baseline_worktree EXIT + trap 'cleanup_baseline_worktree_and_exit 129' HUP + trap 'cleanup_baseline_worktree_and_exit 130' INT + trap 'cleanup_baseline_worktree_and_exit 143' TERM +} + +overlay_current_bench_file() { + local rel_path="$1" + mkdir -p "$(dirname "${baseline_worktree}/${rel_path}")" + rm -rf "${baseline_worktree:?}/${rel_path}" + ln -s "${repo_root}/${rel_path}" "${baseline_worktree}/${rel_path}" +} + +overlay_current_bench_logic() { + overlay_current_bench_file "ci" + overlay_current_bench_file "CMakePresets.json" + overlay_current_bench_file "cmake/CCCLGenerateHeaderTests.cmake" +} + +for arg in "$@"; do + case "$arg" in + -h|-help|--help) usage; exit 0 ;; + *) ;; + esac +done + +new_args="$("${ci_dir}/util/extract_switches.sh" \ + -skip-configure \ + -skip-build \ + -no-tu-csv \ + -prepare-perfetto \ + -no-prepare-perfetto \ + -ctadvisor \ + -- "$@")" + +declare -a new_args="(${new_args})" +set -- "${new_args[@]}" +while true; do + case "$1" in + -skip-configure) skip_configure=1; shift ;; + -skip-build) skip_build=1; shift ;; + -no-tu-csv) write_tu_csv=0; shift ;; + -prepare-perfetto) prepare_perfetto=1; shift ;; + -no-prepare-perfetto) prepare_perfetto=0; shift ;; + -ctadvisor) run_ctadvisor=1; shift ;; + --) shift; break ;; + *) echo "Unknown argument: $1" >&2; usage; exit 1 ;; + esac +done + +while (($#)); do + case "$1" in + -preset) preset="$2"; shift 2 ;; + -target) build_targets+=("$2"); shift 2 ;; + -baseline-ref) baseline_ref="$2"; shift 2 ;; + -tu-csv) explicit_tu_csv=1; write_tu_csv=1; tu_csv="$2"; shift 2 ;; + -perfetto-output) perfetto_output_dir="$2"; shift 2 ;; + -max-detail-len) max_detail_len="$2"; shift 2 ;; + -cloc-processes) cloc_processes="$2"; shift 2 ;; + --) shift; event_args+=("$@"); break ;; + *) common_args+=("$1"); shift ;; + esac +done + +set -- "${common_args[@]}" +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +current_build_root="${BUILD_ROOT}" +current_build_dir="${BUILD_DIR}" + +build_root_for_source() { + local source_root="$1" + mkdir -p "${source_root}/build" + (cd "${source_root}/build" && pwd) +} + +build_dir_for_source() { + local source_root="$1" + local build_root="$2" + local build_dir="${build_root}/${CCCL_BUILD_INFIX}" + mkdir -p "${build_dir}" + readlink -f "${build_dir}" +} + +run_bench_build() { + local source_root="$1" + local build_name="$2" + local build_root="$3" + local build_dir="$4" + + ( + # build_common.sh keeps the active build tree in these globals; override + # them only inside this subshell so current-tree state is restored after + # each build. + # shellcheck disable=SC2030 + BUILD_ROOT="${build_root}" + # shellcheck disable=SC2030 + BUILD_DIR="${build_dir}" + cd "${source_root}/ci" + + if (( ! skip_configure )); then + status "Configuring preset '${preset}' with compile-time bench instrumentation (${build_name})..." + configure_preset "${build_name}" "${preset}" "${bench_cmake_args[@]}" + fi + + if (( ! skip_build )); then + status "Building target(s) (${build_name}): ${build_targets[*]}" + build_preset "${build_name}" "${preset}" --target "${build_targets[@]}" + fi + ) +} + +prepare_perfetto_traces() { + local input_dir="$1" + local output_dir="$2" + local trace_repo_root="$3" + local label="$4" + + status "Preparing Perfetto trace copies (${label})..." + rm -rf "${output_dir}" + "${tool_dir}/prepare_traces.py" \ + --input "${input_dir}" \ + --output "${output_dir}" \ + --repo-root "${trace_repo_root}" \ + --max-detail-len "${max_detail_len}" + status "Perfetto traces (${label}): ${output_dir}" +} + +if ((${#build_targets[@]} == 0)); then + build_targets=("${default_build_targets[@]}") +fi + +preset_build_dir="${current_build_dir}/${preset}" +baseline_trace_dir="" + +if [[ -n "${baseline_ref}" ]]; then + baseline_commit="$(git -C "${repo_root}" rev-parse --verify "${baseline_ref}^{commit}")" + baseline_worktree="$(mktemp -d "${TMPDIR:-/tmp}/cccl-compile-time-baseline.XXXXXX")" + rmdir "${baseline_worktree}" + install_baseline_cleanup_traps + status "Creating baseline worktree for ${baseline_ref} (${baseline_commit})..." + git -C "${repo_root}" worktree add --detach "${baseline_worktree}" "${baseline_commit}" >/dev/null + overlay_current_bench_logic + + baseline_build_root="$(build_root_for_source "${baseline_worktree}")" + baseline_build_dir="$(build_dir_for_source "${baseline_worktree}" "${baseline_build_root}")" + baseline_trace_dir="${baseline_build_dir}/${preset}/compile_time/raw_traces" +fi + +report_root="${preset_build_dir}/compile_time" +trace_dir="${report_root}/raw_traces" +baseline_artifact_trace_dir="${report_root}/baseline_raw_traces" +event_output_dir="${report_root}/event_reports" +tu_csv="${tu_csv:-${report_root}/tu_summary.csv}" +perfetto_output_dir="${perfetto_output_dir:-${report_root}/perfetto_traces}" +if [[ -n "${baseline_ref}" && "${explicit_tu_csv}" -eq 0 ]]; then + write_tu_csv=0 +fi + +require_command python3 +if (( write_tu_csv )); then + require_command cloc +fi +if (( run_ctadvisor )); then + require_command ctadvisor +fi + +run_bench_build "${repo_root}" "Compile-time Bench (current)" "${current_build_root}" "${current_build_dir}" +if $CONFIGURE_ONLY; then + exit 0 +fi + +if [[ -n "${baseline_ref}" ]]; then + run_bench_build \ + "${baseline_worktree}" \ + "Compile-time Bench (baseline)" \ + "${baseline_build_root}" \ + "${baseline_build_dir}" +fi + +shopt -s nullglob globstar +trace_paths=("${trace_dir}"/**/*.json) +(( ${#trace_paths[@]} > 0 )) \ + || { echo "error: no device-time-trace JSON files found under ${trace_dir}" >&2; exit 1; } +if [[ -n "${baseline_ref}" ]]; then + baseline_trace_paths=("${baseline_trace_dir}"/**/*.json) + (( ${#baseline_trace_paths[@]} > 0 )) \ + || { echo "error: no device-time-trace JSON files found under ${baseline_trace_dir}" >&2; exit 1; } + + status "Copying baseline raw traces to ${baseline_artifact_trace_dir}..." + rm -rf "${baseline_artifact_trace_dir}" + mkdir -p "${baseline_artifact_trace_dir}" + cp -a "${baseline_trace_dir}/." "${baseline_artifact_trace_dir}/" +fi + +if (( prepare_perfetto )); then + if [[ -n "${baseline_ref}" ]]; then + rm -rf "${perfetto_output_dir}" + prepare_perfetto_traces \ + "${trace_dir}" \ + "${perfetto_output_dir}/current" \ + "${repo_root}" \ + "current" + prepare_perfetto_traces \ + "${baseline_trace_dir}" \ + "${perfetto_output_dir}/baseline" \ + "${baseline_worktree}" \ + "baseline" + else + prepare_perfetto_traces \ + "${trace_dir}" \ + "${perfetto_output_dir}" \ + "${repo_root}" \ + "current" + fi +fi + +if (( write_tu_csv )); then + status "Writing generated-TU summary CSV..." + "${tool_dir}/summarize_tus.py" \ + --build-dir "${preset_build_dir}" \ + --output-csv "${tu_csv}" \ + --cloc-processes "${cloc_processes}" + status "Generated-TU summary CSV: ${tu_csv}" +fi + +declare -a summary_args=("${event_args[@]}") +if ((${#summary_args[@]} == 0)); then + summary_args=(-f file-processing -e -n 15) +fi +summary_args+=(-o "${event_output_dir}") +if [[ -n "${baseline_ref}" ]]; then + summary_args+=( + --baseline-dir "${baseline_trace_dir}" + --baseline-repo-root "${baseline_worktree}" + ) +fi +status "Writing event summary..." +"${tool_dir}/summarize_events.py" "${trace_dir}" "${summary_args[@]}" + +if (( run_ctadvisor )); then + status "Running ctadvisor over ${#trace_paths[@]} trace(s)..." + ctadvisor \ + --trace-file-path "${trace_dir}" \ + --header-advisor-entries 20 \ + --thread-number "$(nproc --all --ignore=2)" +fi diff --git a/cccl_upstream/ci/build_cub.sh b/cccl_upstream/ci/build_cub.sh new file mode 100755 index 00000000..d647e35b --- /dev/null +++ b/cccl_upstream/ci/build_cub.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +set -euo pipefail + +NO_LID=false +LID0=false +LID1=false +LID2=false +ARTIFACT_TAGS=() + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +new_args="$("${ci_dir}/util/extract_switches.sh" \ + -no-lid \ + -lid0 \ + -lid1 \ + -lid2 \ + -- "$@")" + +declare -a new_args="(${new_args})" +set -- "${new_args[@]}" +while true; do + case "$1" in + -no-lid) + ARTIFACT_TAGS+=("no_lid") + NO_LID=true + shift + ;; + -lid0) + ARTIFACT_TAGS+=("lid_0") + LID0=true + shift + ;; + -lid1) + ARTIFACT_TAGS+=("lid_1") + LID1=true + shift + ;; + -lid2) + ARTIFACT_TAGS+=("lid_2") + LID2=true + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +print_environment_details + +ENABLE_CCCL_BENCHMARKS="false" +ENABLE_CUB_RDC="false" + +if [[ "$CUDA_COMPILER" == *nvcc* ]]; then + ENABLE_CUB_RDC="true" + NVCC_VERSION=$($CUDA_COMPILER --version | grep release | awk '{print $6}' | cut -c2-) + if [[ -n "${DISABLE_CUB_BENCHMARKS}" ]]; then + echo "Benchmarks have been forcefully disabled." + else + ENABLE_CCCL_BENCHMARKS="true" + echo "nvcc version is $NVCC_VERSION. Building CUB benchmarks." + fi +else + echo "Not building with NVCC, disabling RDC and benchmarks." +fi + +if [[ "$HOST_COMPILER" == *icpc* || "$HOST_COMPILER" == *nvhpc* ]]; then + ENABLE_CCCL_BENCHMARKS="false" +fi + +PRESET="cub" +if $NO_LID; then + PRESET="cub-nolid" +elif $LID0; then + PRESET="cub-lid0" +elif $LID1; then + PRESET="cub-lid1" +elif $LID2; then + PRESET="cub-lid2" +fi + +CMAKE_OPTIONS=( + "-DCMAKE_CXX_STANDARD=$CXX_STANDARD" + "-DCMAKE_CUDA_STANDARD=$CXX_STANDARD" + "-DCCCL_ENABLE_BENCHMARKS=$ENABLE_CCCL_BENCHMARKS" + "-DCUB_ENABLE_RDC_TESTS=$ENABLE_CUB_RDC" +) + +configure_and_build_preset "CUB" "$PRESET" "${CMAKE_OPTIONS[@]}" + +# Create test artifacts: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + if [[ ${#ARTIFACT_TAGS[@]} -gt 0 ]]; then + run_command "📦 Packaging test artifacts" \ + "${ci_dir}/upload_cub_test_artifacts.sh" \ + "${ARTIFACT_TAGS[@]}" + else + run_command "📦 Packaging test artifacts" "${ci_dir}/upload_cub_test_artifacts.sh" + fi +fi + +print_time_summary diff --git a/cccl_upstream/ci/build_cuda_cccl_python.sh b/cccl_upstream/ci/build_cuda_cccl_python.sh new file mode 100755 index 00000000..a5994942 --- /dev/null +++ b/cccl_upstream/ci/build_cuda_cccl_python.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage="Usage: $0 -py-version [additional options...]" + +# shellcheck source=ci/util/python/common_arg_parser.sh +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" + +# Check if py_version was provided (this script requires it) +require_py_version "$usage" || exit 1 + +echo "Docker socket: " "$(ls /var/run/docker.sock)" + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + # Prepare mount points etc for getting artifacts in/out of the container. + # shellcheck source=ci/util/artifacts/common.sh + source "$ci_dir/util/artifacts/common.sh" + # Note that these mounts use the runner (not the devcontainer) filesystem for + # source directories because of docker-out-of-docker quirks. + # The workflow-job GH actions make sure that they exist before running any + # scripts. + action_mounts=( + --mount "type=bind,source=${ARTIFACT_ARCHIVES},target=${ARTIFACT_ARCHIVES}" + --mount "type=bind,source=${ARTIFACT_UPLOAD_STAGE},target=${ARTIFACT_UPLOAD_STAGE}" + ) +else + # If not running in GitHub Actions, we don't need to set up artifact mounts. + action_mounts=() +fi + +# cuda_cccl must be built in a container that can produce manylinux wheels, +# and has the CUDA toolkit installed. We use the rapidsai/ci-wheel image for this. +# We build separate wheels using separate containers for each CUDA version, +# then merge them into a single wheel. + +readonly cuda12_version=12.9.1 +readonly cuda13_version=13.1.1 +readonly devcontainer_version=26.04 +readonly devcontainer_distro=rockylinux8 +# Use a baseline Python tag for the rapidsai ci-wheel image. The requested +# py_version is installed inside the container by setup_python_env (uv). +# Pinning the image tag avoids relying on a per-py_version image being +# published (e.g. py3.14 images may not yet exist). +readonly devcontainer_python_version=3.10 + +if [[ "$(uname -m)" == "aarch64" ]]; then + cuda12_image="rapidsai/ci-wheel:${devcontainer_version}-cuda${cuda12_version}-${devcontainer_distro}-py${devcontainer_python_version}-arm64" + cuda13_image="rapidsai/ci-wheel:${devcontainer_version}-cuda${cuda13_version}-${devcontainer_distro}-py${devcontainer_python_version}-arm64" +else + cuda12_image="rapidsai/ci-wheel:${devcontainer_version}-cuda${cuda12_version}-${devcontainer_distro}-py${devcontainer_python_version}" + cuda13_image="rapidsai/ci-wheel:${devcontainer_version}-cuda${cuda13_version}-${devcontainer_distro}-py${devcontainer_python_version}" +fi +# shellcheck disable=SC2034 +readonly cuda12_image +# shellcheck disable=SC2034 +readonly cuda13_image + +mkdir -p wheelhouse + +# Shared caches across the cu12 + cu13 wheel builds. Both jobs compile an +# identical LLVM/clang tree (LLVM has no CUDA dep), so a shared ccache cuts +# the second build's LLVM phase from ~10 min to under 2 min; a shared CPM +# source cache skips the second LLVM git clone entirely. +# +# The `mkdir`s run inside the (dev)container where only the container-side +# paths are visible. The docker bind-mount uses the host-side paths +# (${HOST_WORKSPACE}) since the inner docker daemon is the host's. +mkdir -p ./.ccache ./.cpm-cache +host_ccache_dir="${HOST_WORKSPACE:?}/.ccache" +host_cpm_cache_dir="${HOST_WORKSPACE:?}/.cpm-cache" + +for ctk in 12 13; do + image="cuda${ctk}_image" + image="${!image}" + echo "::group::⚒️ Building CUDA $ctk wheel on $image" + ( + set -x + docker pull "$image" + docker run --rm -i \ + --workdir /workspace/python/cuda_cccl \ + --mount "type=bind,source=${HOST_WORKSPACE:?},target=/workspace/" \ + --mount "type=bind,source=${host_ccache_dir},target=/root/.ccache" \ + --mount "type=bind,source=${host_cpm_cache_dir},target=/root/.cpm-cache" \ + "${action_mounts[@]}" \ + --env "py_version=${py_version}" \ + --env "GITHUB_ACTIONS=${GITHUB_ACTIONS:-}" \ + --env "GITHUB_RUN_ID=${GITHUB_RUN_ID:-}" \ + --env "JOB_ID=${JOB_ID:-}" \ + --env "CCCL_PYTHON_USE_V2=${CCCL_PYTHON_USE_V2:-}" \ + --env "CCCL_C_PARALLEL_SANITIZE_THREAD=${CCCL_C_PARALLEL_SANITIZE_THREAD:-}" \ + --env "CCACHE_DIR=/root/.ccache" \ + --env "CPM_SOURCE_CACHE=/root/.cpm-cache" \ + "$image" \ + /workspace/ci/build_cuda_cccl_wheel.sh + # Prevent GHA runners from exhausting available storage with leftover images: + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + docker rmi -f "$image" + fi + ) + echo "::endgroup::" +done + +echo "Merging CUDA wheels..." + +# Set up a Python environment for the merge/repair steps. +source "$ci_dir/pyenv_helper.sh" +setup_python_env "${py_version}" + +# Needed for unpacking and repacking wheels. +python -m pip install wheel + +# Find the built wheels +cu12_wheel=$(find wheelhouse -name "*cu12*.whl" | head -1) +cu13_wheel=$(find wheelhouse -name "*cu13*.whl" | head -1) + +if [[ -z "$cu12_wheel" ]]; then + echo "Error: CUDA 12 wheel not found in wheelhouse/" + ls -la wheelhouse/ + exit 1 +fi + +if [[ -z "$cu13_wheel" ]]; then + echo "Error: CUDA 13 wheel not found in wheelhouse/" + ls -la wheelhouse/ + exit 1 +fi + +echo "Found CUDA 12 wheel: $cu12_wheel" +echo "Found CUDA 13 wheel: $cu13_wheel" + +# Merge the wheels +python python/cuda_cccl/merge_cuda_wheels.py "$cu12_wheel" "$cu13_wheel" --output-dir wheelhouse_merged + +# A ThreadSanitizer wheel links libtsan; keep it external (excluded) so it is +# NOT bundled -- the TSan test lane LD_PRELOADs the runner's matching libtsan +# instead. Harmless for normal builds (the .so has no libtsan dependency). +tsan_exclude=() +if [[ "${CCCL_C_PARALLEL_SANITIZE_THREAD:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + tsan_exclude=(--exclude 'libtsan.so.2') +fi + +# Install auditwheel and repair the merged wheel +python -m pip install patchelf auditwheel +for wheel in wheelhouse_merged/cuda_cccl-*.whl; do + echo "Repairing merged wheel: $wheel" + python -m auditwheel repair \ + --exclude 'libnvrtc.so.12' \ + --exclude 'libnvrtc.so.13' \ + --exclude 'libnvJitLink.so.12' \ + --exclude 'libnvJitLink.so.13' \ + --exclude 'libcudart.so.12' \ + --exclude 'libcudart.so.13' \ + --exclude 'libcuda.so.1' \ + "${tsan_exclude[@]}" \ + "$wheel" \ + --wheel-dir wheelhouse_final +done + +# Clean up intermediate files and move only the final merged wheel to wheelhouse +rm -rf wheelhouse/* # Clean existing wheelhouse +mkdir -p wheelhouse + +# Move only the final repaired merged wheel +if ls wheelhouse_final/cuda_cccl-*.whl 1> /dev/null 2>&1; then + mv wheelhouse_final/cuda_cccl-*.whl wheelhouse/ + echo "Final merged wheel moved to wheelhouse" +else + echo "No final repaired wheel found, moving unrepaired merged wheel" + mv wheelhouse_merged/cuda_cccl-*.whl wheelhouse/ +fi + +# Clean up temporary directories +rm -rf wheelhouse_merged wheelhouse_final + +echo "Final wheels in wheelhouse:" +ls -la wheelhouse/ + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name="$(ci/util/workflow/get_wheel_artifact_name.sh)" + ci/util/artifacts/upload.sh "$wheel_artifact_name" 'wheelhouse/.*' +fi diff --git a/cccl_upstream/ci/build_cuda_cccl_python_tsan.sh b/cccl_upstream/ci/build_cuda_cccl_python_tsan.sh new file mode 100755 index 00000000..cec4273e --- /dev/null +++ b/cccl_upstream/ci/build_cuda_cccl_python_tsan.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Thin wrapper around build_cuda_cccl_python.sh that builds the cuda_cccl wheel +# with ThreadSanitizer instrumentation on the c.parallel (v1) host code, for the +# free-threaded (3.14t) TSan nightly lane. The shared build script honors +# CCCL_C_PARALLEL_SANITIZE_THREAD (passes -DCCCL_C_PARALLEL_SANITIZE_THREAD=ON to +# the wheel build and --exclude libtsan from the auditwheel repair). +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CCCL_C_PARALLEL_SANITIZE_THREAD=1 +"$ci_dir/build_cuda_cccl_python.sh" "$@" diff --git a/cccl_upstream/ci/build_cuda_cccl_python_v2.sh b/cccl_upstream/ci/build_cuda_cccl_python_v2.sh new file mode 100755 index 00000000..a62f2aa2 --- /dev/null +++ b/cccl_upstream/ci/build_cuda_cccl_python_v2.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Thin wrapper around build_cuda_cccl_python.sh that builds the cuda_cccl +# wheel against the HostJIT-based cccl.c.parallel.v2 library instead of the +# legacy NVRTC v1 library. The shared build script honors CCCL_PYTHON_USE_V2. +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CCCL_PYTHON_USE_V2=1 +exec "$ci_dir/build_cuda_cccl_python.sh" "$@" diff --git a/cccl_upstream/ci/build_cuda_cccl_wheel.sh b/cccl_upstream/ci/build_cuda_cccl_wheel.sh new file mode 100755 index 00000000..5fe15685 --- /dev/null +++ b/cccl_upstream/ci/build_cuda_cccl_wheel.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Target script for `docker run` command in build_cuda_cccl_python.sh +# The /workspace pathnames are hard-wired here. + +# Toolchain version, pinned in one place. Every gcc-toolset package name and the +# /opt/rh enable path below derive from it — the -fsanitize=thread libtsan must +# come from the same toolset as the compiler, so these cannot drift apart. +readonly gcc_toolset_version=13 + +# Install the GCC toolset (needed for the build) and ccache (shared between +# cu12 and cu13 builds via /root/.ccache bind-mount from the host). +/workspace/ci/util/retry.sh 5 30 dnf -y install \ + "gcc-toolset-${gcc_toolset_version}-gcc" "gcc-toolset-${gcc_toolset_version}-gcc-c++" ccache + +# ThreadSanitizer builds link libcccl.c.parallel.so with -fsanitize=thread, which +# the linker resolves via the toolset's libtsan.so. That runtime lives in a +# separate package not pulled in by -gcc-c++, so install it for the TSan lane. +if [[ "${CCCL_C_PARALLEL_SANITIZE_THREAD:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + /workspace/ci/util/retry.sh 5 30 dnf -y install "gcc-toolset-${gcc_toolset_version}-libtsan-devel" +fi + +# When the caller bind-mounts a ccache dir, wire it through to CMake. This +# transparently caches every compile, so the second wheel build (cu13 after +# cu12, or vice versa) reuses the entire LLVM/clang object tree. +if [[ -n "${CCACHE_DIR:-}" ]]; then + export CMAKE_C_COMPILER_LAUNCHER=ccache + export CMAKE_CXX_COMPILER_LAUNCHER=ccache + export CMAKE_CUDA_COMPILER_LAUNCHER=ccache + echo "ccache enabled: CCACHE_DIR=${CCACHE_DIR}" + ccache --version 2>&1 | head -1 || true + ccache --show-stats 2>&1 | head -5 || true +fi +echo -e "#!/usr/bin/env bash\nsource /opt/rh/gcc-toolset-${gcc_toolset_version}/enable" >/etc/profile.d/enable_devtools.sh +# shellcheck disable=SC1091 +source /etc/profile.d/enable_devtools.sh + +# Check what's available +command -v gcc +gcc --version +command -v nvcc +nvcc --version + +# Set up Python environment +# shellcheck source=ci/pyenv_helper.sh +source /workspace/ci/pyenv_helper.sh +# shellcheck disable=SC2154 +setup_python_env "${py_version}" +command -v python +python --version +echo "Done setting up python env" + +# Figure out the version to use for the package, we need repo history +if "$(git rev-parse --is-shallow-repository)"; then + git fetch --unshallow +fi +export PACKAGE_VERSION_PREFIX="0.1." +package_version=$(/workspace/ci/generate_version.sh) +echo "Using package version ${package_version}" +# Override the version used by setuptools_scm to the custom version +export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_CCCL="${package_version}" + +cd /workspace/python/cuda_cccl + +# Determine CUDA version from nvcc +cuda_version=$(nvcc --version | grep -oP 'release \K[0-9]+\.[0-9]+' | cut -d. -f1) +echo "Detected CUDA version: ${cuda_version}" + +# Configure compilers: +CXX="$(command -v g++)" +export CXX +CUDACXX="$(command -v nvcc)" +export CUDACXX +CUDAHOSTCXX="$(command -v g++)" +export CUDAHOSTCXX + +# When CCCL_PYTHON_USE_V2 is set (=1/true/on), build the wheel against the +# HostJIT-based cccl.c.parallel.v2 library instead of the default v1. +if [[ "${CCCL_PYTHON_USE_V2:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + export CMAKE_ARGS="${CMAKE_ARGS:-} -DCCCL_PYTHON_USE_V2=ON" + echo "Building wheel with CCCL v2 backend: CMAKE_ARGS=${CMAKE_ARGS}" + + # v2's hostjit links against libnvJitLink and libnvfatbin, which aren't in + # the base rapidsai/ci-wheel image. Install the matching CTK devel packages + # so CMake's FindCUDAToolkit picks them up. nvcc is on PATH; derive the + # version (e.g. "13-0") from it. + ctk_pkg_ver=$(nvcc --version 2>/dev/null \ + | grep -oP 'release \K[0-9]+\.[0-9]+' | tr '.' '-') + if [[ -n "${ctk_pkg_ver}" ]]; then + echo "Installing libnvjitlink-devel-${ctk_pkg_ver} libnvfatbin-devel-${ctk_pkg_ver}..." + /workspace/ci/util/retry.sh 5 30 dnf -y install \ + "libnvjitlink-devel-${ctk_pkg_ver}" \ + "libnvfatbin-devel-${ctk_pkg_ver}" + else + echo "WARNING: could not derive CTK version from nvcc; skipping nvJitLink/nvfatbin install" + fi + + # FindCUDAToolkit learned about CUDA::nvfatbin only in CMake 3.27. The base + # rapidsai/ci-wheel image ships an older CMake; install a newer one into + # the active venv so scikit-build-core picks it up over the system cmake. + echo "Pinning cmake>=3.27 for FindCUDAToolkit nvfatbin support..." + python -m pip install --upgrade 'cmake>=3.27' +fi + +# When CCCL_C_PARALLEL_SANITIZE_THREAD is set (=1/true/on), instrument the +# c.parallel (v1) host code with ThreadSanitizer for the free-threaded TSan +# nightly lane. Host-only; the libtsan runtime stays external (the shared +# build_cuda_cccl_python.sh --excludes it from auditwheel). +if [[ "${CCCL_C_PARALLEL_SANITIZE_THREAD:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + export CMAKE_ARGS="${CMAKE_ARGS:-} -DCCCL_C_PARALLEL_SANITIZE_THREAD=ON" + echo "Building wheel with ThreadSanitizer-instrumented c.parallel: CMAKE_ARGS=${CMAKE_ARGS}" +fi + +# Build the wheel +python -m pip wheel --no-deps --verbose --wheel-dir dist . + +# Rename wheel to include CUDA version suffix +for wheel in dist/cuda_cccl-*.whl; do + if [[ -f "$wheel" ]]; then + base_name=$(basename "$wheel" .whl) + new_name="${base_name}.cu${cuda_version}.whl" + mv "$wheel" "dist/${new_name}" + echo "Renamed wheel to: ${new_name}" + fi +done + +# Move wheel to output directory +mkdir -p /workspace/wheelhouse +mv dist/cuda_cccl-*.cu*.whl /workspace/wheelhouse/ diff --git a/cccl_upstream/ci/build_cudax.sh b/cccl_upstream/ci/build_cudax.sh new file mode 100755 index 00000000..8d8794ed --- /dev/null +++ b/cccl_upstream/ci/build_cudax.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +# If the cudax_ENABLE_CUFILE variable is specified, we don't modify it. Otherwise if we got an nvcc binary, check the +# nvcc version and if it's less than 12.9, disable the cuFile support. NVHPC Toolkit doesn't come with cuFile, too, so +# we don't enable cuFile support if the host compiler is nvc++. +if [[ -z "${cudax_ENABLE_CUFILE:-}" ]]; then + cudax_ENABLE_CUFILE="false" + if [[ -n "${NVCC_VERSION:-}" ]] && [[ "$(basename "${HOST_COMPILER}")" != "nvc++" ]]; then + if util/version_compare.sh "${NVCC_VERSION}" ge 12.9; then + cudax_ENABLE_CUFILE="true" + fi + fi +fi + +PRESET="cudax" + +CMAKE_OPTIONS=( + "-Dcudax_ENABLE_CUFILE=${cudax_ENABLE_CUFILE}" + "-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" + "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}" +) + +configure_and_build_preset "CUDA Experimental" "$PRESET" "${CMAKE_OPTIONS[@]}" + +print_time_summary diff --git a/cccl_upstream/ci/build_libcudacxx.sh b/cccl_upstream/ci/build_libcudacxx.sh new file mode 100755 index 00000000..29a8708a --- /dev/null +++ b/cccl_upstream/ci/build_libcudacxx.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +print_environment_details + +PRESET="libcudacxx" +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") + +upload_test_artifacts=false +if [[ -n "${GITHUB_ACTIONS:-}" ]] && "${ci_dir}/util/workflow/has_consumers.sh"; then + upload_test_artifacts=true + export LIT_OPTS="${LIT_OPTS:+${LIT_OPTS} }-Dtest_executable_mode=build" +fi + +configure_and_build_preset libcudacxx "$PRESET" "${CMAKE_OPTIONS[@]}" + +if $upload_test_artifacts; then + run_command "📦 Packaging test artifacts" "${ci_dir}/upload_libcudacxx_test_artifacts.sh" +fi + +print_time_summary diff --git a/cccl_upstream/ci/build_stdpar.sh b/cccl_upstream/ci/build_stdpar.sh new file mode 100755 index 00000000..7a58326b --- /dev/null +++ b/cccl_upstream/ci/build_stdpar.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Ensure the script is being executed in the root cccl directory: +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.."; + +# Get the current CCCL info: +readonly cccl_repo="${PWD}" +readonly workdir="${cccl_repo}/test/stdpar" + +CXX_STANDARD=17 + +args=("$@") +while [[ "${#args[@]}" -ne 0 ]]; do + case "${args[0]}" in + -std) CXX_STANDARD="${args[1]}"; args=("${args[@]:2}");; + *) echo "Unrecognized option: ${args[0]}"; exit 1 ;; + esac +done + +mkdir -p "${workdir}" +cd "${workdir}" + +# Configure and build +rm -rf build + +cmake -B build -S . -G Ninja \ + -DCMAKE_CXX_STANDARD="${CXX_STANDARD}" \ + `# Explicitly compile for hopper since the CI machine does not have a gpu:` \ + -DCMAKE_CXX_FLAGS="-gpu=cc90" + +# Disabled because `cmake --build -j ""` is invalid, but so is +# `cmake --build -j8`. CMake expects a space between `-j` and +# the numeric argument, or no argument at all. +# shellcheck disable=SC2086 +cmake --build build -j ${PARALLEL_LEVEL:-} diff --git a/cccl_upstream/ci/build_thrust.sh b/cccl_upstream/ci/build_thrust.sh new file mode 100755 index 00000000..b3250614 --- /dev/null +++ b/cccl_upstream/ci/build_thrust.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +PRESET="thrust" + +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=$CXX_STANDARD" "-DCMAKE_CUDA_STANDARD=$CXX_STANDARD") + +configure_and_build_preset "Thrust" "$PRESET" "${CMAKE_OPTIONS[@]}" + +# Create test artifacts: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + run_command "📦 Packaging test artifacts" /home/coder/cccl/ci/upload_thrust_test_artifacts.sh +fi + +print_time_summary diff --git a/cccl_upstream/ci/build_tidy.sh b/cccl_upstream/ci/build_tidy.sh new file mode 100755 index 00000000..a2e1a5cb --- /dev/null +++ b/cccl_upstream/ci/build_tidy.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +BUILD_NAME="clang-tidy" +PRESET="all-tidy" +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") +# Clang does not understand -G, passed by all-dev-debug which all-tidy derives from +CMAKE_OPTIONS+=("-DCMAKE_CUDA_FLAGS=") +# TODO(jfaibussowit) +# +# STF seems to trip clang-cuda up pretty heavily. It's unclear whether this is because STF +# hasn't been compiled against clang-cuda before or whether it's an issue with clang-cuda +# itself. +CMAKE_OPTIONS+=("-Dcudax_ENABLE_CUDASTF=OFF") +CMAKE_OPTIONS+=("-Dcudax_ENABLE_PLACES=OFF") + +# todo(dabayer): Re-enable OpenMP thrust builds for clang-tidy. +CMAKE_OPTIONS+=("-DTHRUST_MULTICONFIG_ENABLE_SYSTEM_OMP=OFF") + +# Cannot use configure_and_build_preset because that does not allow us to pass additional +# arguments to the build command. +configure_preset "${BUILD_NAME}" "${PRESET}" "${CMAKE_OPTIONS[@]}" +# Keep going after errors, we want CI to unearth all clang-tidy errors in one go +BUILD_OPTIONS=(-- -k 0) +build_preset "${BUILD_NAME}" "${PRESET}" "${BUILD_OPTIONS[@]}" + +print_time_summary diff --git a/cccl_upstream/ci/compile_time/README.md b/cccl_upstream/ci/compile_time/README.md new file mode 100644 index 00000000..96c417bd --- /dev/null +++ b/cccl_upstream/ci/compile_time/README.md @@ -0,0 +1,77 @@ +# Compile-time benchmark CI contracts + +The compile-time benchmark CI flow is configured from `ci/matrix.yaml` under +`compile_time.pull_request`. + +## Matrix schema + +Each config is a GitHub Actions matrix entry: + +```yaml +compile_time: + pull_request: + - id: public-headers-gcc13 + name: Public headers compile-time bench + gpu: rtx2080 + launch_args: "--cuda 13.3 --host gcc13" + baseline_ref: origin/main + preset: all-dev + targets: + - cub.headers.base + args: "-arch native" + slices: + - id: total-compilation + title: TU total compilation + filter: total-compilation + timing: inclusive + sort: total + top: 15 + threshold: 0.001 +``` + +Required config fields are `id`, `name`, `gpu`, `launch_args`, +`baseline_ref`, `preset`, `targets`, and `slices`. `args`, `comment`, and +`artifact_retention_days` are optional. + +Required slice fields are `id`, `title`, `filter`, `timing`, `sort`, `top`, and +`threshold`. Slice `children` may be used to group nested report sections in the +PR comment. Empty slice sections are omitted recursively by the renderer unless +the summary manifest carries warnings for that slice. + +`ci/compile_time/parse_matrix.py ci/matrix.yaml --workflow pull_request` emits +the GitHub Actions matrix JSON. Missing or empty `compile_time.pull_request` +emits `{"include":[]}`. + +In baseline comparisons, `threshold` is measured against the total selected +inclusive/exclusive impact across all matched traces. The per-side reports still +use `sort` for their own top-N ordering; comparison worse/better tables always +rank by total impact so a change repeated across many traces is not hidden by a +larger single-trace movement. + +## Report contract + +`summarize_events.py --slices ` writes per-slice CSVs under +`event_reports//` and writes a normalized `event_reports/summary.json` +manifest. The manifest is the renderer contract; CSVs are human artifacts. +Configured slices that match no events, have no matching trace files, or have no +comparable event keys record warnings in the manifest so reporting failures are +not presented as ordinary no-regression results. + +In comparison mode, the wrapper preserves: + +- current raw traces: `compile_time/raw_traces` +- baseline raw traces: `compile_time/baseline_raw_traces` +- Perfetto copies: `compile_time/perfetto_traces/current` and + `compile_time/perfetto_traces/baseline` + +## PR comments + +`render_pr_comment.py` reads `summary.json`, config metadata, and an artifacts +URL, then writes the sticky PR comment body. Regressions and improvements are +rendered in separate `
` blocks and are never mixed in one table. +Warnings are rendered separately and keep their slice visible even when there +are no regression/improvement rows. + +The reusable workflow uses the sticky-comment header +`compile-time-bench-` with `hide_and_recreate: true`, so previous +comments for the same config are archived as outdated. diff --git a/cccl_upstream/ci/compile_time/analytics.ipynb b/cccl_upstream/ci/compile_time/analytics.ipynb new file mode 100644 index 00000000..369411a8 --- /dev/null +++ b/cccl_upstream/ci/compile_time/analytics.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "903704f7", + "metadata": {}, + "source": [ + "# Compile-Time Analytics\n", + "\n", + "This notebook helps you:\n", + "\n", + "1. Run `ci/build_compile_time_bench.sh` from the repo root.\n", + "2. Load and inspect an all-header processing CSV.\n", + "3. Explore high-impact headers by TU coverage and average processing time.\n", + "4. Build combined ranking scores to identify optimization targets.\n", + "\n", + "Expected CSV columns include:\n", + "- `header_path`\n", + "- `include_tu_count`\n", + "- `avg_process_time_s`\n", + "- `total_process_time_s`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad58db28", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install pandas matplotlib plotly nbformat" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16ba6808", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import shlex\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "\n", + "pd.set_option(\"display.max_colwidth\", 160)\n", + "pd.set_option(\"display.width\", 200)\n", + "pd.set_option(\"display.max_columns\", 20)\n", + "\n", + "REPO_ROOT = Path.cwd()\n", + "while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n", + " REPO_ROOT = REPO_ROOT.parent\n", + "\n", + "if not (REPO_ROOT / \".git\").exists():\n", + " raise RuntimeError(\n", + " \"Could not locate repo root (.git). Start notebook from inside the CCCL repo.\"\n", + " )\n", + "\n", + "print(f\"Repo root: {REPO_ROOT}\")\n", + "print(f\"Python executable: {sys.executable}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373652c8", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Run build_compile_time_bench.sh (file-processing mode) ---\n", + "import subprocess\n", + "from pathlib import Path\n", + "\n", + "if \"REPO_ROOT\" not in globals():\n", + " REPO_ROOT = Path.cwd()\n", + " while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n", + " REPO_ROOT = REPO_ROOT.parent\n", + "\n", + "output_csv = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n", + "cmd = [\n", + " \"bash\",\n", + " str(REPO_ROOT / \"ci\" / \"build_compile_time_bench.sh\"),\n", + " *shlex.split(os.environ.get(\"COMPILE_TIME_BUILD_ARGS\", \"\")),\n", + " \"--\",\n", + " \"-f\",\n", + " \"file-processing\",\n", + " \"-e\",\n", + " \"-n\",\n", + " os.environ.get(\"COMPILE_TIME_TOP_N\", \"5000\"),\n", + " \"--sort\",\n", + " \"total\",\n", + " \"--output-csv\",\n", + " str(output_csv),\n", + "]\n", + "\n", + "print(\"Command:\")\n", + "print(\" \" + \" \".join(shlex.quote(x) for x in cmd))\n", + "\n", + "subprocess.run(cmd, cwd=REPO_ROOT, check=True)\n", + "print(f\"\\nWrote: {output_csv}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12ad4c5c", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Load CSV ---\n", + "csv_path = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n", + "if not csv_path.exists():\n", + " raise FileNotFoundError(f\"Missing CSV: {csv_path}. Run the script first.\")\n", + "\n", + "df = pd.read_csv(csv_path)\n", + "\n", + "event_summary_cols = {\n", + " \"event_name\",\n", + " \"event_key\",\n", + " \"root_tu_count\",\n", + " \"selected_avg_per_root_tu_s\",\n", + " \"selected_total_s\",\n", + "}\n", + "required_cols = [\n", + " \"header_path\",\n", + " \"include_tu_count\",\n", + " \"avg_process_time_s\",\n", + " \"total_process_time_s\",\n", + "]\n", + "\n", + "if event_summary_cols.issubset(df.columns):\n", + " df[\"header_path\"] = df[\"event_key\"]\n", + " df[\"include_tu_count\"] = df[\"root_tu_count\"]\n", + " if (\n", + " \"avg_inclusive_per_root_tu_s\" in df.columns\n", + " and \"total_inclusive_s\" in df.columns\n", + " ):\n", + " df[\"avg_process_time_s\"] = df[\"avg_inclusive_per_root_tu_s\"]\n", + " df[\"total_process_time_s\"] = df[\"total_inclusive_s\"]\n", + " else:\n", + " df[\"avg_process_time_s\"] = df[\"selected_avg_per_root_tu_s\"]\n", + " df[\"total_process_time_s\"] = df[\"selected_total_s\"]\n", + "\n", + "missing = [c for c in required_cols if c not in df.columns]\n", + "if missing:\n", + " raise ValueError(\n", + " f\"CSV is not all-header processing output. Missing columns: {missing}. \"\n", + " \"Run the script cell above to regenerate /tmp/compile_time.csv.\"\n", + " )\n", + "\n", + "for col in [\"include_tu_count\", \"avg_process_time_s\", \"total_process_time_s\"]:\n", + " df[col] = pd.to_numeric(df[col], errors=\"coerce\").fillna(0)\n", + "\n", + "print(f\"Rows: {len(df):,}\")\n", + "print(f\"Columns: {list(df.columns)}\")\n", + "df.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a9b36a8", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Quick top-N views ---\n", + "TOP_N = 5\n", + "\n", + "print(\"Top by avg_process_time_s\")\n", + "display(\n", + " df.nlargest(TOP_N, \"avg_process_time_s\")[\n", + " [\n", + " \"header_path\",\n", + " \"avg_process_time_s\",\n", + " \"include_tu_count\",\n", + " \"total_process_time_s\",\n", + " ]\n", + " ]\n", + ")\n", + "\n", + "print(\"\\nTop by include_tu_count\")\n", + "display(\n", + " df.nlargest(TOP_N, \"include_tu_count\")[\n", + " [\n", + " \"header_path\",\n", + " \"include_tu_count\",\n", + " \"avg_process_time_s\",\n", + " \"total_process_time_s\",\n", + " ]\n", + " ]\n", + ")\n", + "\n", + "print(\"\\nTop by impact score (include_tu_count * avg_process_time_s)\")\n", + "df_score = df.copy()\n", + "df_score[\"impact_score\"] = df_score[\"include_tu_count\"] * df_score[\"avg_process_time_s\"]\n", + "display(\n", + " df_score.nlargest(TOP_N, \"impact_score\")[\n", + " [\n", + " \"header_path\",\n", + " \"impact_score\",\n", + " \"include_tu_count\",\n", + " \"avg_process_time_s\",\n", + " \"total_process_time_s\",\n", + " ]\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "174eabd4", + "metadata": {}, + "source": [ + "### How to read this plot\n", + "\n", + "- Each point is one header seen in all-mode profiling.\n", + "- **X axis (`include_tu_count`)**: how many generated public-header TUs include this header at least once.\n", + "- **Y axis (`avg_process_time_s`)**: average time spent processing that header per including TU.\n", + "- Headers near the **upper-right** are usually the best optimization candidates because they are both widespread and expensive per include." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ddc0725", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Interactive scatter (hover shows header name) ---\n", + "\n", + "import plotly.express as px\n", + "\n", + "plot_df = df.copy()\n", + "\n", + "total_headers = len(plot_df)\n", + "total_public_headers = int(plot_df[\"include_tu_count\"].max())\n", + "\n", + "fig = px.scatter(\n", + " plot_df,\n", + " x=\"include_tu_count\",\n", + " y=\"avg_process_time_s\",\n", + " hover_name=\"header_path\",\n", + " hover_data={\n", + " \"include_tu_count\": True,\n", + " \"avg_process_time_s\": \":.6f\",\n", + " \"total_process_time_s\": \":.3f\",\n", + " \"header_path\": False,\n", + " },\n", + " opacity=0.6,\n", + " title=(\n", + " f\"Header include count vs avg processing time ({total_headers:,} total headers; \"\n", + " f\"coverage measured across {total_public_headers} public headers)\"\n", + " ),\n", + ")\n", + "\n", + "fig.update_layout(\n", + " xaxis_title=(\n", + " \"TU coverage: number of public headers that include this header \"\n", + " f\"(out of {total_public_headers})\"\n", + " ),\n", + " yaxis_title=\"Average processing time per including TU (seconds)\",\n", + " height=900,\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f429b934", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Optional: run ctadvisor and parse expensive headers ---\n", + "run_ctadvisor = os.environ.get(\"COMPILE_TIME_RUN_CTADVISOR\") == \"1\"\n", + "CTADVISOR_ENTRIES = 10\n", + "CTADVISOR_THREADS = os.cpu_count() or 8\n", + "\n", + "trace_root = (\n", + " REPO_ROOT\n", + " / \"build\"\n", + " / os.environ.get(\"CCCL_BUILD_INFIX\", \"cuda13.1-gcc14\")\n", + " / os.environ.get(\"CCCL_COMPILE_TIME_PRESET\", \"all-dev\")\n", + " / \"compile_time\"\n", + " / \"raw_traces\"\n", + ")\n", + "ctadvisor_cmd = [\n", + " \"ctadvisor\",\n", + " \"--trace-file-path\",\n", + " str(trace_root),\n", + " \"--header-advisor-entries\",\n", + " str(CTADVISOR_ENTRIES),\n", + " \"--thread-number\",\n", + " str(CTADVISOR_THREADS),\n", + "]\n", + "\n", + "if run_ctadvisor:\n", + " print(\"Command:\")\n", + " print(\" \" + \" \".join(shlex.quote(x) for x in ctadvisor_cmd))\n", + "\n", + " result = subprocess.run(\n", + " ctadvisor_cmd, cwd=REPO_ROOT, check=True, capture_output=True, text=True\n", + " )\n", + " print(result.stdout)\n", + "else:\n", + " print(\"Skipping ctadvisor. Set COMPILE_TIME_RUN_CTADVISOR=1 to run it.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cccl", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/cccl_upstream/ci/compile_time/parse_matrix.py b/cccl_upstream/ci/compile_time/parse_matrix.py new file mode 100755 index 00000000..f9e20837 --- /dev/null +++ b/cccl_upstream/ci/compile_time/parse_matrix.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +try: + import yaml +except ModuleNotFoundError: + yaml = None + YAML_ERROR_TYPES: tuple[type[BaseException], ...] = () +else: + YAML_ERROR_TYPES = (yaml.YAMLError,) + +ID_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]*$") +TIMINGS = {"inclusive", "exclusive"} +SORTS = {"total", "avg", "avg-root-tu", "max"} + + +def die(message: str) -> None: + print(f"error: {message}", file=sys.stderr) + raise SystemExit(2) + + +def require_mapping(value: Any, where: str) -> dict[str, Any]: + if not isinstance(value, dict): + die(f"{where} must be a mapping") + return value + + +def require_field(mapping: dict[str, Any], field: str, where: str) -> Any: + if field not in mapping: + die(f"{where} is missing required field '{field}'") + return mapping[field] + + +def require_string(value: Any, where: str, *, nonempty: bool = True) -> str: + if not isinstance(value, str): + die(f"{where} must be a string") + if nonempty and not value: + die(f"{where} must be non-empty") + return value + + +def require_id(value: Any, where: str) -> str: + text = require_string(value, where) + if not ID_RE.fullmatch(text): + die(f"{where} must match {ID_RE.pattern}") + return text + + +def require_string_list(value: Any, where: str) -> list[str]: + if not isinstance(value, list) or not value: + die(f"{where} must be a non-empty list") + strings: list[str] = [] + for index, item in enumerate(value): + strings.append(require_string(item, f"{where}[{index}]")) + return strings + + +def require_bool(value: Any, where: str) -> bool: + if not isinstance(value, bool): + die(f"{where} must be a boolean") + return value + + +def require_positive_int(value: Any, where: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + die(f"{where} must be a positive integer") + return value + + +def validate_slice( + slice_data: Any, + *, + where: str, + seen_ids: set[str], +) -> dict[str, Any]: + data = require_mapping(slice_data, where) + slice_id = require_id(require_field(data, "id", where), f"{where}.id") + if slice_id in seen_ids: + die(f"duplicate slice id '{slice_id}' in {where}") + seen_ids.add(slice_id) + + title = require_string(require_field(data, "title", where), f"{where}.title") + filter_name = require_string( + require_field(data, "filter", where), f"{where}.filter" + ) + timing = require_string(require_field(data, "timing", where), f"{where}.timing") + if timing not in TIMINGS: + die(f"{where}.timing must be one of {sorted(TIMINGS)}") + sort = require_string(require_field(data, "sort", where), f"{where}.sort") + if sort not in SORTS: + die(f"{where}.sort must be one of {sorted(SORTS)}") + + top = require_field(data, "top", where) + if isinstance(top, bool) or not isinstance(top, int) or top <= 0: + die(f"{where}.top must be a positive integer") + threshold = require_field(data, "threshold", where) + if ( + isinstance(threshold, bool) + or not isinstance(threshold, (int, float)) + or threshold < 0 + ): + die(f"{where}.threshold must be a non-negative number") + + result: dict[str, Any] = { + "id": slice_id, + "title": title, + "filter": filter_name, + "timing": timing, + "sort": sort, + "top": top, + "threshold": threshold, + } + for optional in ("scope_filter", "exclusive_scope"): + if optional in data: + result[optional] = require_string( + data[optional], f"{where}.{optional}", nonempty=False + ) + + children = data.get("children", []) + if not isinstance(children, list): + die(f"{where}.children must be a list") + if children: + result["children"] = [ + validate_slice( + child, + where=f"{where}.children[{index}]", + seen_ids=seen_ids, + ) + for index, child in enumerate(children) + ] + return result + + +def validate_config( + config_data: Any, *, where: str, seen_ids: set[str] +) -> dict[str, Any]: + data = require_mapping(config_data, where) + config_id = require_id(require_field(data, "id", where), f"{where}.id") + if config_id in seen_ids: + die(f"duplicate compile_time config id '{config_id}'") + seen_ids.add(config_id) + + targets = require_string_list( + require_field(data, "targets", where), f"{where}.targets" + ) + slices = require_field(data, "slices", where) + if not isinstance(slices, list) or not slices: + die(f"{where}.slices must be a non-empty list") + + slice_ids: set[str] = set() + normalized_slices = [ + validate_slice( + slice_data, + where=f"{where}.slices[{index}]", + seen_ids=slice_ids, + ) + for index, slice_data in enumerate(slices) + ] + + return { + "id": config_id, + "name": require_string(require_field(data, "name", where), f"{where}.name"), + "gpu": require_string(require_field(data, "gpu", where), f"{where}.gpu"), + "launch_args": require_string( + require_field(data, "launch_args", where), f"{where}.launch_args" + ), + "baseline_ref": require_string( + require_field(data, "baseline_ref", where), f"{where}.baseline_ref" + ), + "preset": require_string( + require_field(data, "preset", where), f"{where}.preset" + ), + "targets": targets, + "args": require_string(data.get("args", ""), f"{where}.args", nonempty=False), + "comment": require_bool(data.get("comment", True), f"{where}.comment"), + "artifact_retention_days": require_positive_int( + data.get("artifact_retention_days", 14), + f"{where}.artifact_retention_days", + ), + "slices": normalized_slices, + } + + +def matrix_entry(config: dict[str, Any]) -> dict[str, Any]: + config_id = config["id"] + return { + "id": config_id, + "name": config["name"], + "gpu": config["gpu"], + "launch_args": config["launch_args"], + "baseline_ref": config["baseline_ref"], + "preset": config["preset"], + "targets_json": json.dumps(config["targets"], separators=(",", ":")), + "args": config["args"], + "slices_json": json.dumps({"slices": config["slices"]}, separators=(",", ":")), + "comment": str(config["comment"]).lower(), + "artifact_retention_days": config["artifact_retention_days"], + "comment_header": f"compile-time-bench-{config_id}", + } + + +def parse_matrix(path: Path, workflow: str) -> dict[str, Any]: + try: + if yaml is not None: + with path.open(encoding="utf-8") as f: + matrix = yaml.safe_load(f) or {} + else: + completed = subprocess.run( + ["yq", "-o=json", ".", path.as_posix()], + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + matrix = json.loads(completed.stdout or "{}") + except OSError as e: + die(f"failed to read {path}: {e}") + except subprocess.CalledProcessError as e: + die(f"failed to parse {path} with yq: {e.stderr.strip()}") + except json.JSONDecodeError as e: + die(f"failed to decode {path} as JSON: {e}") + except YAML_ERROR_TYPES as e: + die(f"failed to parse {path}: {e}") + + compile_time = matrix.get("compile_time") + if compile_time is None: + return {"include": []} + compile_time = require_mapping(compile_time, "compile_time") + configs = compile_time.get(workflow, []) + if configs is None: + configs = [] + if not isinstance(configs, list): + die(f"compile_time.{workflow} must be a list") + if not configs: + return {"include": []} + + seen_ids: set[str] = set() + return { + "include": [ + matrix_entry( + validate_config( + config, + where=f"compile_time.{workflow}[{index}]", + seen_ids=seen_ids, + ) + ) + for index, config in enumerate(configs) + ] + } + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Parse ci/matrix.yaml compile_time entries for GitHub Actions." + ) + parser.add_argument("matrix_yaml", type=Path) + parser.add_argument("--workflow", default="pull_request") + args = parser.parse_args() + + json.dump(parse_matrix(args.matrix_yaml, args.workflow), sys.stdout) + print() + + +if __name__ == "__main__": + main() diff --git a/cccl_upstream/ci/compile_time/prepare_traces.py b/cccl_upstream/ci/compile_time/prepare_traces.py new file mode 100755 index 00000000..672ffe5c --- /dev/null +++ b/cccl_upstream/ci/compile_time/prepare_traces.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import argparse +import json +from pathlib import Path + +DETAIL_EVENT_NAMES = { + "Code Generation Function", + "CodeGen Function", + "ExecuteCompiler", + "Frontend", + "Instantiating Template Class", + "Instantiating Template Function", + "InstantiateClass", + "InstantiateFunction", + "OptFunction", + "ParseClass", + "PerformPendingInstantiations", + "Processing Header File", + "RunPass", + "Scanning Function Body", + "Source", +} + +DETAIL_PREFIXES_TO_STRIP = ( + "libcudacxx/include/", + "cudax/include/", + "c/parallel/include/", +) + +DETAIL_PREFIXES_TO_COLLAPSE = ( + ("cub/cub/", "cub/"), + ("thrust/thrust/", "thrust/"), +) + + +def normalize_detail(detail: str, repo_root: Path) -> str: + detail_path = Path(detail) + if detail_path.is_absolute(): + try: + rel = detail_path.resolve(strict=False).relative_to(repo_root) + detail = rel.as_posix() + except ValueError: + pass + + for prefix in DETAIL_PREFIXES_TO_STRIP: + if detail.startswith(prefix): + detail = detail[len(prefix) :] + break + + for prefix, replacement in DETAIL_PREFIXES_TO_COLLAPSE: + if detail.startswith(prefix): + detail = replacement + detail[len(prefix) :] + break + + return detail + + +def display_detail(detail: str, repo_root: Path, max_detail_len: int | None) -> str: + detail = normalize_detail(detail, repo_root) + if ( + max_detail_len is not None + and max_detail_len > 0 + and len(detail) > max_detail_len + ): + return detail[: max_detail_len - 1] + "..." + return detail + + +def rewrite_event_name( + event: dict, repo_root: Path, max_detail_len: int | None +) -> bool: + name = event.get("name") + if name not in DETAIL_EVENT_NAMES: + return False + + args = event.get("args") + if not isinstance(args, dict): + return False + + detail = args.get("detail") + if not detail: + return False + + args.setdefault("original_name", name) + event["name"] = f"{name}: {display_detail(str(detail), repo_root, max_detail_len)}" + return True + + +def prepare_trace( + input_path: Path, output_path: Path, repo_root: Path, max_detail_len: int | None +) -> int: + with input_path.open(encoding="utf-8") as f: + trace = json.load(f) + + rewritten = 0 + for event in trace.get("traceEvents", []): + if rewrite_event_name(event, repo_root, max_detail_len): + rewritten += 1 + + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w", encoding="utf-8") as f: + json.dump(trace, f, separators=(",", ":")) + + return rewritten + + +def iter_input_traces(input_path: Path) -> list[Path]: + if input_path.is_file(): + return [input_path] + return sorted(input_path.rglob("*.json")) + + +def output_path_for(input_trace: Path, input_root: Path, output_path: Path) -> Path: + if input_root.is_file(): + if output_path.is_dir() or not output_path.suffix: + return output_path / f"{input_trace.stem}.perfetto.json" + return output_path + + rel = input_trace.relative_to(input_root) + return output_path / rel.parent / f"{rel.stem}.perfetto.json" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Prepare NVCC device-time-trace JSON files for Perfetto by promoting args.detail into event names." + ) + parser.add_argument( + "--input", required=True, type=Path, help="Input trace JSON file or directory" + ) + parser.add_argument( + "--output", required=True, type=Path, help="Output trace JSON file or directory" + ) + parser.add_argument( + "--repo-root", default=Path(__file__).resolve().parents[2], type=Path + ) + parser.add_argument( + "--max-detail-len", + default=0, + type=int, + help="Truncate promoted detail text to this many characters; 0 keeps full details", + ) + args = parser.parse_args() + + input_path = args.input.resolve(strict=False) + output_path = args.output.resolve(strict=False) + repo_root = args.repo_root.resolve(strict=False) + max_detail_len = args.max_detail_len if args.max_detail_len > 0 else None + + traces = iter_input_traces(input_path) + if not traces: + raise SystemExit(f"no JSON traces found under {args.input}") + + total_rewritten = 0 + for trace_path in traces: + total_rewritten += prepare_trace( + trace_path, + output_path_for(trace_path, input_path, output_path), + repo_root, + max_detail_len, + ) + + print(f"prepared {len(traces)} trace(s); renamed {total_rewritten} event(s)") + + +if __name__ == "__main__": + main() diff --git a/cccl_upstream/ci/compile_time/render_pr_comment.py b/cccl_upstream/ci/compile_time/render_pr_comment.py new file mode 100755 index 00000000..6d86fd75 --- /dev/null +++ b/cccl_upstream/ci/compile_time/render_pr_comment.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 + +import argparse +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as f: + payload = json.load(f) + if not isinstance(payload, dict): + raise SystemExit(f"{path} must contain a JSON object") + return payload + + +def md_escape(value: object) -> str: + text = str(value) + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("|", "\\|") + .replace("\n", " ") + ) + + +def md_code_span(value: object) -> str: + text = str(value).replace("\n", " ") + max_backtick_run = 0 + current_backtick_run = 0 + for char in text: + if char == "`": + current_backtick_run += 1 + max_backtick_run = max(max_backtick_run, current_backtick_run) + else: + current_backtick_run = 0 + delimiter = "`" * (max_backtick_run + 1) + if text.startswith("`") or text.endswith("`"): + text = f" {text} " + return f"{delimiter}{text}{delimiter}" + + +def render_event_name(row: dict[str, Any]) -> str: + event_name = row.get("event_name", "") + event_key = row.get("event_key", "") + if event_key: + return f"{md_escape(event_name)}: {md_code_span(event_key)}" + return md_escape(event_name) + + +def render_rows(rows: list[dict[str, Any]], *, direction: str) -> str: + delta_heading = ( + "Regression impact" if direction == "worse" else "Improvement impact" + ) + lines = [ + f"| Rank | {delta_heading} | Selected Δ | Baseline | Current | Event | Matched traces |", + "| ---: | ---: | ---: | ---: | ---: | --- | ---: |", + ] + for row in rows: + lines.append( + "| {rank} | `{impact}` | `{selected_delta}` | `{baseline}` | `{current}` | {event} | {traces} |".format( + rank=md_escape(row.get("rank", "")), + impact=md_escape(row.get("impact_magnitude_s", "")), + selected_delta=md_escape(row.get("selected_delta_s", "")), + baseline=md_escape(row.get("baseline_selected_s", "")), + current=md_escape(row.get("current_selected_s", "")), + event=render_event_name(row), + traces=md_escape(row.get("matched_trace_count", "")), + ) + ) + return "\n".join(lines) + + +def render_direction_details( + slice_title: str, + direction: str, + rows: list[dict[str, Any]], +) -> str: + if not rows: + return "" + label = "Regressions" if direction == "worse" else "Improvements" + icon = "🔴" if direction == "worse" else "🟢" + return "\n".join( + [ + "
", + f"{icon} {md_escape(slice_title)} — {label}", + "", + render_rows(rows, direction=direction), + "", + "
", + ] + ) + + +def render_warning_details(slice_title: str, warnings: list[Any]) -> str: + if not warnings: + return "" + lines = [ + "
", + f"⚠️ {md_escape(slice_title)} — Warnings", + "", + ] + lines.extend(f"- {md_escape(warning)}" for warning in warnings) + lines.extend(["", "
"]) + return "\n".join(lines) + + +def render_slice(slice_data: dict[str, Any], *, level: int = 3) -> str: + comparison = slice_data.get("comparison", {}) + worse_rows = comparison.get("worse", {}).get("rows", []) + better_rows = comparison.get("better", {}).get("rows", []) + warnings = slice_data.get("warnings", []) + child_sections = [ + rendered + for child in slice_data.get("children", []) + if (rendered := render_slice(child, level=level + 1)) + ] + direct_sections = [ + section + for section in ( + render_warning_details(slice_data.get("title", "Slice"), warnings), + render_direction_details( + slice_data.get("title", "Slice"), "worse", worse_rows + ), + render_direction_details( + slice_data.get("title", "Slice"), "better", better_rows + ), + ) + if section + ] + if not direct_sections and not child_sections: + return "" + + heading_prefix = "#" * min(level, 6) + subtitle = ( + f"`-f {slice_data.get('filter', '')}` " + f"`{slice_data.get('timing', '')}` " + f"`--sort {slice_data.get('sort', '')}`" + ) + lines = [ + f"{heading_prefix} {md_escape(slice_data.get('title', 'Slice'))}", + "", + subtitle, + "", + ] + lines.extend(join_sections(direct_sections)) + if child_sections: + lines.extend(["", *join_sections(child_sections)]) + return "\n".join(lines).strip() + + +def join_sections(sections: list[str]) -> list[str]: + lines: list[str] = [] + for section in sections: + if lines: + lines.append("") + lines.append(section) + return lines + + +def count_rows(slice_data: dict[str, Any], direction: str) -> int: + comparison = slice_data.get("comparison", {}) + total = len(comparison.get(direction, {}).get("rows", [])) + return total + sum( + count_rows(child, direction) for child in slice_data.get("children", []) + ) + + +def count_warnings(slice_data: dict[str, Any]) -> int: + return len(slice_data.get("warnings", [])) + sum( + count_warnings(child) for child in slice_data.get("children", []) + ) + + +def render_comment( + summary: dict[str, Any], + config: dict[str, Any], + *, + artifacts_url: str, +) -> str: + config_id = str(config["id"]) + slices = summary.get("slices", []) + sections = [ + section for slice_data in slices if (section := render_slice(slice_data)) + ] + worse_count = sum(count_rows(slice_data, "worse") for slice_data in slices) + better_count = sum(count_rows(slice_data, "better") for slice_data in slices) + warning_count = sum(count_warnings(slice_data) for slice_data in slices) + result = ( + f"**Result:** {worse_count} regression row(s), " + f"{better_count} improvement row(s) above threshold." + ) + if warning_count: + result += f" {warning_count} warning(s)." + + lines = [ + f"", + f"## ⏱️ CCCL compile-time benchmark comparison: {md_escape(config.get('name', config_id))}", + "", + result, + "", + "| Run | Value |", + "| --- | --- |", + f"| Config | {md_code_span(config_id)} |", + f"| Baseline | {md_code_span(config.get('baseline_ref', ''))} |", + f"| Preset | {md_code_span(config.get('preset', ''))} |", + f"| Targets | {md_code_span(', '.join(config.get('targets', [])))} |", + f"| GPU / launch args | {md_code_span(config.get('gpu', ''))} / {md_code_span(config.get('launch_args', ''))} |", + "", + f"**Artifacts:** [reports and traces]({artifacts_url})", + "", + ] + if sections: + lines.extend(join_sections(sections)) + else: + lines.append( + "No compile-time benchmark changes exceeded the configured thresholds." + ) + return "\n".join(lines).rstrip() + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Render a GitHub PR comment from compile-time report JSON." + ) + parser.add_argument("--summary", type=Path, required=True) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--artifacts-url", required=True) + parser.add_argument("-o", "--output", type=Path) + args = parser.parse_args() + + comment = render_comment( + load_json(args.summary), + load_json(args.config), + artifacts_url=args.artifacts_url, + ) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(comment, encoding="utf-8") + else: + print(comment, end="") + + +if __name__ == "__main__": + main() diff --git a/cccl_upstream/ci/compile_time/summarize_events.py b/cccl_upstream/ci/compile_time/summarize_events.py new file mode 100755 index 00000000..f591bc87 --- /dev/null +++ b/cccl_upstream/ci/compile_time/summarize_events.py @@ -0,0 +1,1718 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import heapq +import json +import re +from collections import defaultdict +from dataclasses import dataclass, field, replace +from pathlib import Path +from typing import Any, Callable + +DEFAULT_SCOPE_FILTER = r"(^|[^A-Za-z0-9_:])(?:::)?(?:cuda|thrust|cub|cccl)::" +SYMBOL_SCOPE_EVENT_NAMES = { + "Scanning Function Body", + "Instantiating Template Class", + "Instantiating Template Function", + "Generating Function IR", + "OptFunction", +} +ITANIUM_CV_QUALIFIERS = frozenset("KOVR") + + +@dataclass(frozen=True) +class FilterSpec: + label: str + description: str + matches: Callable[["TraceEvent"], bool] + default_exclusive_scope: str = "all" + + +@dataclass(frozen=True) +class ReportConfig: + slice_id: str + title: str + spec: FilterSpec + timing: str + exclusive_scope: str + sort_by: str + top_n: int + tag: str | None + threshold_us: float = 0.0 + scope_filter: re.Pattern[str] | None = None + + +@dataclass +class TraceEvent: + name: str + detail: str + start_us: int + end_us: int + pid: int + tid: int + root_tu: str + synthetic: bool = False + children: list["TraceEvent"] = field(default_factory=list) + + @property + def inclusive_us(self) -> int: + return self.end_us - self.start_us + + def key(self, repo_root: Path) -> str: + if self.detail: + return normalize_detail(self.detail, repo_root) + return self.name + + +@dataclass +class EventStats: + event_name: str + event_key: str + event_count: int = 0 + total_inclusive_us: int = 0 + total_exclusive_us: int = 0 + max_inclusive_us: int = 0 + max_exclusive_us: int = 0 + trace_paths: set[str] = field(default_factory=set) + root_tus: set[str] = field(default_factory=set) + + +@dataclass +class ComparisonStats: + event_name: str + event_key: str + baseline: EventStats + current: EventStats + matched_trace_paths: set[str] = field(default_factory=set) + + +@dataclass(frozen=True) +class ComparisonRow: + stats: ComparisonStats + baseline_impact_us: float + current_impact_us: float + baseline_metric_us: float + current_metric_us: float + impact_magnitude_us: float + + @property + def impact_delta_us(self) -> float: + return self.current_impact_us - self.baseline_impact_us + + @property + def selected_delta_us(self) -> float: + return self.current_metric_us - self.baseline_metric_us + + @property + def selected_magnitude_us(self) -> float: + return abs(self.selected_delta_us) + + +@dataclass(frozen=True) +class ComparisonSide: + name: str + repo_root: Path + events: list[TraceEvent] + report_ids: set[tuple[str, str]] + child_ids: set[tuple[str, str]] + + +@dataclass(frozen=True) +class ComparisonInput: + name: str + trace_paths: dict[Path, Path] + repo_root: Path + + +@dataclass(frozen=True) +class ReportSide: + name: str + trace_paths: list[Path] + repo_root: Path + output_dir: Path + + +@dataclass(frozen=True) +class SliceRequest: + config: ReportConfig + filter_name: str + children: tuple["SliceRequest", ...] = () + + +def merged_interval_duration(intervals: list[tuple[int, int]]) -> int: + if not intervals: + return 0 + + sorted_intervals = sorted(intervals) + total = 0 + merged_start, merged_end = sorted_intervals[0] + for start, end in sorted_intervals[1:]: + if start > merged_end: + total += merged_end - merged_start + merged_start, merged_end = start, end + else: + merged_end = max(merged_end, end) + + total += merged_end - merged_start + return total + + +def generated_tu_input(tu: str) -> str: + marker = "/headers/" + if marker not in tu: + return tu + + rel = tu.split(marker, 1)[1] + parts = rel.split("/", 1) + if len(parts) != 2: + return tu + + tu_input = parts[1] + for suffix in (".cu", ".cpp", ".cxx", ".cc", ".c"): + if tu_input.endswith(suffix): + return tu_input[: -len(suffix)] + + return tu_input + + +def normalize_detail(detail: str, repo_root: Path) -> str: + detail_path = Path(detail) + if detail_path.is_absolute(): + try: + detail = detail_path.resolve(strict=False).relative_to(repo_root).as_posix() + except ValueError: + pass + + return detail + + +def normalize_project_file(detail: str, repo_root: Path) -> str | None: + detail_path = Path(detail) + if not detail_path.is_absolute(): + return None + + try: + detail = detail_path.resolve(strict=False).relative_to(repo_root).as_posix() + except ValueError: + return None + + if detail.startswith("build/"): + return None + return detail + + +def general_event_identity(event: TraceEvent, repo_root: Path) -> tuple[str, str]: + return (event.name, event.key(repo_root)) + + +def strip_angle_arguments(symbol: str) -> str: + stripped: list[str] = [] + depth = 0 + for char in symbol: + if char == "<": + depth += 1 + continue + if char == ">" and depth: + depth -= 1 + continue + if depth == 0: + stripped.append(char) + return "".join(stripped) + + +def symbol_name_prefix(symbol: str) -> str: + before_parameters = strip_angle_arguments(symbol).split("(", 1)[0].strip() + if not before_parameters: + return symbol + if "::operator" in before_parameters: + operator_scope = before_parameters.rfind("::operator") + prefix_start = before_parameters.rfind(" ", 0, operator_scope) + return before_parameters[prefix_start + 1 :] + return before_parameters.rsplit(None, 1)[-1] + + +def itanium_nested_scope_candidates(symbol: str) -> list[str]: + if not symbol.startswith("_Z"): + return [] + + candidates: list[str] = [] + for nested_marker in (i for i, char in enumerate(symbol) if char == "N"): + index = nested_marker + 1 + while index < len(symbol) and symbol[index] in ITANIUM_CV_QUALIFIERS: + index += 1 + + scopes: list[str] = [] + while index < len(symbol) and symbol[index].isdigit(): + length_start = index + while index < len(symbol) and symbol[index].isdigit(): + index += 1 + try: + component_length = int(symbol[length_start:index]) + except ValueError: + break + + component = symbol[index : index + component_length] + if len(component) != component_length: + break + + scopes.append(component) + candidates.append("::".join(scopes) + "::") + index += component_length + + return candidates + + +def symbol_scope_candidates(event: TraceEvent) -> list[str]: + if event.name not in SYMBOL_SCOPE_EVENT_NAMES or not event.detail: + return [] + + candidates = [symbol_name_prefix(event.detail)] + if " [" in event.detail: + _, bracketed_symbol = event.detail.split(" [", 1) + candidates.append(symbol_name_prefix(bracketed_symbol.rstrip("]"))) + + for candidate in list(candidates): + candidates.extend(itanium_nested_scope_candidates(candidate)) + + return candidates + + +def matches_scope_filter( + event: TraceEvent, scope_filter: re.Pattern[str] | None +) -> bool: + if scope_filter is None or event.name not in SYMBOL_SCOPE_EVENT_NAMES: + return True + return any( + scope_filter.search(candidate) for candidate in symbol_scope_candidates(event) + ) + + +def matches_report_config(event: TraceEvent, config: ReportConfig) -> bool: + return config.spec.matches(event) and matches_scope_filter( + event, config.scope_filter + ) + + +def report_event_identity( + event: TraceEvent, config: ReportConfig, repo_root: Path +) -> tuple[str, str] | None: + if not matches_report_config(event, config): + return None + + if config.spec.label == "file-processing": + event_key = normalize_project_file(event.detail, repo_root) + if event_key is None: + return None + else: + event_key = event.key(repo_root) + + return (event.name, event_key) + + +def filter_event_identity( + event: TraceEvent, config: ReportConfig, repo_root: Path +) -> tuple[str, str] | None: + if not matches_report_config(event, config): + return None + return general_event_identity(event, repo_root) + + +def trace_root_tu(trace: dict, trace_path: Path) -> str: + input_files = trace.get("otherData", {}).get("inputFiles", []) + if input_files: + return generated_tu_input(input_files[0]) + return trace_path.as_posix() + + +def iter_trace_paths(trace_dir: Path) -> list[Path]: + return sorted(p for p in trace_dir.rglob("*.json") if p.is_file()) + + +def iter_duration_events(trace_path: Path, repo_root: Path) -> list[TraceEvent]: + with trace_path.open(encoding="utf-8") as f: + trace = json.load(f) + + root_tu = normalize_detail(trace_root_tu(trace, trace_path), repo_root) + events: list[TraceEvent] = [] + for event in trace.get("traceEvents", []): + if event.get("ph") not in (None, "X"): + continue + if "ts" not in event or "dur" not in event: + continue + + name = str(event.get("name", "")) + if not name: + continue + + args = event.get("args", {}) + detail = "" + if isinstance(args, dict): + detail = str(args.get("detail", "") or "") + + start_us = int(event["ts"]) + dur_us = int(event["dur"]) + events.append( + TraceEvent( + name=name, + detail=detail, + start_us=start_us, + end_us=start_us + dur_us, + pid=int(event.get("pid", 0)), + tid=int(event.get("tid", 0)), + root_tu=root_tu, + ) + ) + + if events: + trace_start_us = min(event.start_us for event in events) + trace_end_us = max(event.end_us for event in events) + events.append( + TraceEvent( + name="Total Compilation Time", + detail=root_tu, + start_us=trace_start_us, + end_us=trace_end_us, + pid=-1, + tid=-1, + root_tu=root_tu, + synthetic=True, + ) + ) + + return events + + +def link_child_events(events: list[TraceEvent]) -> None: + grouped: dict[tuple[int, int], list[TraceEvent]] = defaultdict(list) + for event in events: + grouped[(event.pid, event.tid)].append(event) + + for thread_events in grouped.values(): + stack: list[TraceEvent] = [] + for event in sorted(thread_events, key=lambda e: (e.start_us, -e.end_us)): + # Example: A=[0,10], B=[10,20]. A ended before B starts, so it is + # not B's parent. + while stack and stack[-1].end_us <= event.start_us: + stack.pop() + # Example: A=[0,100], B=[50,150]. B overlaps A but is not fully + # contained by A, so A is not B's parent. + while stack and not ( + stack[-1].start_us <= event.start_us + and event.end_us <= stack[-1].end_us + ): + stack.pop() + + # Example: A=[0,100], B=[0,100]. Identical-span events are not + # treated as nested children of each other. + if stack and ( + stack[-1].start_us != event.start_us or stack[-1].end_us != event.end_us + ): + stack[-1].children.append(event) + + stack.append(event) + + +def read_trace_events(trace_path: Path, repo_root: Path) -> list[TraceEvent]: + events = iter_duration_events(trace_path, repo_root) + link_child_events(events) + return events + + +def event_name_filter( + *, label: str, description: str, event_names: tuple[str, ...] +) -> FilterSpec: + name_set = set(event_names) + return FilterSpec( + label=label, + description=description, + matches=lambda event: event.name in name_set, + ) + + +def any_event_filter() -> FilterSpec: + return FilterSpec( + label="all", + description="all raw duration events", + matches=lambda event: not event.synthetic, + ) + + +def regex_filter(pattern: str) -> FilterSpec: + compiled = re.compile(pattern, re.IGNORECASE) + return FilterSpec( + label=f"regex-{slugify(pattern)}", + description=f"event name or detail matches /{pattern}/i", + matches=lambda event: ( + bool(compiled.search(event.name)) or bool(compiled.search(event.detail)) + ) + and not event.synthetic, + ) + + +def builtin_filters() -> dict[str, FilterSpec]: + filters: dict[str, FilterSpec] = {} + + def add(spec: FilterSpec) -> None: + filters[spec.label] = spec + + add(any_event_filter()) + add( + FilterSpec( + label="file-processing", + description=( + "PHF trace events; exclusive time subtracts nested PHF events " + "to match the direct file-processing metric" + ), + matches=lambda event: event.name == "Processing Header File", + default_exclusive_scope="same-filter", + ), + ) + add( + event_name_filter( + label="scanning-function-body", + description="Scanning Function Body events", + event_names=("Scanning Function Body",), + ), + ) + add( + event_name_filter( + label="template-instantiation", + description="template class/function instantiation events", + event_names=( + "Instantiating Template Class", + "Instantiating Template Function", + ), + ), + ) + add( + event_name_filter( + label="template-class-instantiation", + description="template class instantiation events", + event_names=("Instantiating Template Class",), + ), + ) + add( + event_name_filter( + label="template-function-instantiation", + description="template function instantiation events", + event_names=("Instantiating Template Function",), + ), + ) + add( + event_name_filter( + label="pending-instantiations", + description="pending template instantiation phase events", + event_names=("Generating Needed Template Instantiations",), + ), + ) + add( + event_name_filter( + label="frontend", + description="front-end phase events", + event_names=( + "Front End Cleanup", + "CUDA C++ Front-End", + ), + ), + ) + add( + event_name_filter( + label="host-compiler", + description="host compiler preprocessing and compiling events", + event_names=( + "g++ (preprocessing 1)", + "g++ (preprocessing 4)", + "g++ (compiling)", + "gcc (preprocessing 1)", + "gcc (preprocessing 4)", + "gcc (compiling)", + ), + ), + ) + add( + event_name_filter( + label="code-generation", + description="code generation events", + event_names=( + "Generating Function IR", + "Generating NVVM IR", + "NVVM CodeGen", + ), + ), + ) + add( + event_name_filter( + label="optimizer", + description="optimizer events", + event_names=( + "OptFunction", + "NVVM Optimizer", + ), + ), + ) + add( + event_name_filter( + label="total-compilation", + description=( + "synthetic per-trace wall-clock span from first to last timed " + "trace event" + ), + event_names=("Total Compilation Time",), + ), + ) + + return filters + + +def resolve_filter(filter_name: str) -> FilterSpec: + filters = builtin_filters() + normalized = filter_name.strip().lower() + if normalized in filters: + return filters[normalized] + + return regex_filter(filter_name) + + +def exclusive_child_events(event: TraceEvent, config: ReportConfig) -> list[TraceEvent]: + if config.exclusive_scope == "all": + return event.children + if config.exclusive_scope == "same-filter": + return [ + child for child in event.children if matches_report_config(child, config) + ] + raise ValueError(f"unknown exclusive scope: {config.exclusive_scope}") + + +def event_exclusive_us(event: TraceEvent, config: ReportConfig) -> int: + child_intervals = [ + (child.start_us, child.end_us) + for child in exclusive_child_events(event, config) + ] + return max(0, event.inclusive_us - merged_interval_duration(child_intervals)) + + +def collect_stats( + trace_paths: list[Path], + repo_root: Path, + config: ReportConfig, +) -> dict[tuple[str, str], EventStats]: + stats: dict[tuple[str, str], EventStats] = {} + + for trace_path in trace_paths: + events = read_trace_events(trace_path, repo_root) + collect_trace_stats( + stats, + events, + trace_path.as_posix(), + repo_root, + report_config=config, + exclusive_config=config, + ) + + return stats + + +def collect_trace_stats( + stats: dict[tuple[str, str], EventStats], + events: list[TraceEvent], + trace_path: str, + repo_root: Path, + *, + report_config: ReportConfig, + exclusive_config: ReportConfig, +) -> None: + for event in events: + identity = report_event_identity(event, report_config, repo_root) + if identity is None: + continue + add_event_stats( + stats, + identity, + event.inclusive_us, + event_exclusive_us(event, exclusive_config), + trace_path, + event.root_tu, + ) + + +def add_event_stats( + stats: dict[tuple[str, str], EventStats], + identity: tuple[str, str], + inclusive_us: int, + exclusive_us: int, + trace_path: str, + root_tu: str, +) -> None: + event_name, event_key = identity + event_stats = stats.setdefault( + identity, EventStats(event_name=event_name, event_key=event_key) + ) + merge_event_stats( + event_stats, + EventStats( + event_name=event_name, + event_key=event_key, + event_count=1, + total_inclusive_us=inclusive_us, + total_exclusive_us=exclusive_us, + max_inclusive_us=inclusive_us, + max_exclusive_us=exclusive_us, + trace_paths={trace_path}, + root_tus={root_tu}, + ), + ) + + +def selected_total_us(stats: EventStats, timing: str) -> int: + if timing == "inclusive": + return stats.total_inclusive_us + if timing == "exclusive": + return stats.total_exclusive_us + raise ValueError(f"unknown timing: {timing}") + + +def average_us(total_us: int, count: int) -> float: + if count == 0: + return 0.0 + return total_us / count + + +def selected_avg_us(stats: EventStats, timing: str) -> float: + return average_us(selected_total_us(stats, timing), stats.event_count) + + +def selected_avg_per_root_tu_us(stats: EventStats, timing: str) -> float: + return average_us(selected_total_us(stats, timing), len(stats.root_tus)) + + +def selected_max_us(stats: EventStats, timing: str) -> int: + if timing == "inclusive": + return stats.max_inclusive_us + if timing == "exclusive": + return stats.max_exclusive_us + raise ValueError(f"unknown timing: {timing}") + + +def selected_metric_us(stats: EventStats, timing: str, sort_by: str) -> float: + if sort_by == "total": + return float(selected_total_us(stats, timing)) + if sort_by == "avg": + return selected_avg_us(stats, timing) + if sort_by == "avg-root-tu": + return selected_avg_per_root_tu_us(stats, timing) + if sort_by == "max": + return float(selected_max_us(stats, timing)) + raise ValueError(f"unknown sort: {sort_by}") + + +def trace_paths_by_relative_root(trace_dir: Path) -> dict[Path, Path]: + return {path.relative_to(trace_dir): path for path in iter_trace_paths(trace_dir)} + + +def comparison_input(name: str, trace_dir: Path, repo_root: Path) -> ComparisonInput: + return ComparisonInput( + name=name, + trace_paths=trace_paths_by_relative_root(trace_dir), + repo_root=repo_root, + ) + + +def comparable_child_identities( + events: list[TraceEvent], + repo_root: Path, + config: ReportConfig, +) -> set[tuple[str, str]]: + if config.exclusive_scope == "all": + return {general_event_identity(event, repo_root) for event in events} + if config.exclusive_scope == "same-filter": + return { + identity + for event in events + if (identity := filter_event_identity(event, config, repo_root)) is not None + } + raise ValueError(f"unknown exclusive scope: {config.exclusive_scope}") + + +def comparable_report_filter( + config: ReportConfig, + repo_root: Path, + comparable_report_ids: set[tuple[str, str]], +) -> FilterSpec: + def matches(event: TraceEvent) -> bool: + identity = report_event_identity(event, config, repo_root) + return identity is not None and identity in comparable_report_ids + + return replace(config.spec, matches=matches) + + +def comparable_child_filter( + config: ReportConfig, + repo_root: Path, + comparable_child_ids: set[tuple[str, str]], +) -> FilterSpec: + def child_identity(event: TraceEvent) -> tuple[str, str] | None: + if config.exclusive_scope == "all": + return general_event_identity(event, repo_root) + if config.exclusive_scope == "same-filter": + return filter_event_identity(event, config, repo_root) + raise ValueError(f"unknown exclusive scope: {config.exclusive_scope}") + + def matches(event: TraceEvent) -> bool: + identity = child_identity(event) + return identity is not None and identity in comparable_child_ids + + return replace(config.spec, matches=matches) + + +def merge_event_stats(target: EventStats, source: EventStats) -> None: + target.event_count += source.event_count + target.total_inclusive_us += source.total_inclusive_us + target.total_exclusive_us += source.total_exclusive_us + target.max_inclusive_us = max(target.max_inclusive_us, source.max_inclusive_us) + target.max_exclusive_us = max(target.max_exclusive_us, source.max_exclusive_us) + target.trace_paths.update(source.trace_paths) + target.root_tus.update(source.root_tus) + + +def merge_comparison_side_stats( + comparison_stats: dict[tuple[str, str], ComparisonStats], + side_name: str, + side_stats: dict[tuple[str, str], EventStats], +) -> None: + for identity, source_stats in side_stats.items(): + event_name, event_key = identity + comparison = comparison_stats.setdefault( + identity, + ComparisonStats( + event_name=event_name, + event_key=event_key, + baseline=EventStats(event_name, event_key), + current=EventStats(event_name, event_key), + ), + ) + target_stats = getattr(comparison, side_name) + merge_event_stats(target_stats, source_stats) + comparison.matched_trace_paths.update(source_stats.trace_paths) + + +def read_comparison_side( + name: str, + trace_path: Path, + repo_root: Path, + config: ReportConfig, +) -> ComparisonSide: + events = read_trace_events(trace_path, repo_root) + report_ids = { + identity + for event in events + if (identity := report_event_identity(event, config, repo_root)) is not None + } + child_ids = comparable_child_identities(events, repo_root, config) + return ComparisonSide( + name=name, + repo_root=repo_root, + events=events, + report_ids=report_ids, + child_ids=child_ids, + ) + + +def collect_comparison_stats( + baseline_trace_dir: Path, + current_trace_dir: Path, + baseline_repo_root: Path, + current_repo_root: Path, + config: ReportConfig, +) -> tuple[dict[tuple[str, str], ComparisonStats], int]: + comparison_inputs = ( + comparison_input("baseline", baseline_trace_dir, baseline_repo_root), + comparison_input("current", current_trace_dir, current_repo_root), + ) + matched_rel_paths = sorted( + set.intersection( + *( + set(comparison_input.trace_paths) + for comparison_input in comparison_inputs + ) + ) + ) + comparison_stats: dict[tuple[str, str], ComparisonStats] = {} + + for rel_path in matched_rel_paths: + sides = tuple( + read_comparison_side( + comparison_input.name, + comparison_input.trace_paths[rel_path], + comparison_input.repo_root, + config, + ) + for comparison_input in comparison_inputs + ) + + comparable_report_ids = set.intersection(*(side.report_ids for side in sides)) + if not comparable_report_ids: + continue + + comparable_child_ids = set.intersection(*(side.child_ids for side in sides)) + rel_path_str = rel_path.as_posix() + + for side in sides: + report_config = replace( + config, + spec=comparable_report_filter( + config, side.repo_root, comparable_report_ids + ), + ) + exclusive_config = replace( + config, + spec=comparable_child_filter( + config, side.repo_root, comparable_child_ids + ), + exclusive_scope="same-filter", + scope_filter=( + config.scope_filter + if config.exclusive_scope == "same-filter" + else None + ), + ) + side_stats: dict[tuple[str, str], EventStats] = {} + collect_trace_stats( + side_stats, + side.events, + rel_path_str, + side.repo_root, + report_config=report_config, + exclusive_config=exclusive_config, + ) + merge_comparison_side_stats(comparison_stats, side.name, side_stats) + + return comparison_stats, len(matched_rel_paths) + + +def sorted_rows( + stats: dict[tuple[str, str], EventStats], config: ReportConfig +) -> list[EventStats]: + if config.sort_by not in ("total", "avg", "avg-root-tu", "max"): + raise ValueError(f"unknown sort: {config.sort_by}") + + # Python has heapq.nsmallest rather than a C++-style partial_sort. The + # selected metric is negated here so "smallest" means "largest selected + # time", while the string tie-breakers keep their natural ascending order. + return heapq.nsmallest( + config.top_n, + stats.values(), + key=lambda item: ( + -selected_metric_us(item, config.timing, config.sort_by), + item.event_name, + item.event_key, + ), + ) + + +def seconds(us: float | int) -> str: + return f"{us / 1_000_000.0:.6f}" + + +def slugify(value: str) -> str: + slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", value.strip()).strip("-").lower() + return slug or "report" + + +def default_output_path( + output_dir: Path, + config: ReportConfig, +) -> Path: + pieces = ["top", str(config.top_n), config.spec.label, config.timing] + if config.timing == "exclusive": + pieces.append(config.exclusive_scope) + pieces.append(f"by-{config.sort_by}") + if config.tag: + pieces.append(slugify(config.tag)) + return output_dir / ("-".join(slugify(piece) for piece in pieces) + ".csv") + + +def comparison_output_path( + output_dir: Path, + config: ReportConfig, + direction: str, +) -> Path: + pieces = ["top", str(config.top_n), config.spec.label, config.timing] + if config.timing == "exclusive": + pieces.append(config.exclusive_scope) + pieces.extend([f"by-{config.sort_by}", direction]) + if config.tag: + pieces.append(slugify(config.tag)) + return output_dir / ("-".join(slugify(piece) for piece in pieces) + ".csv") + + +def event_stats_csv_row(rank: int, row: EventStats, timing: str) -> dict[str, object]: + root_tu_count = len(row.root_tus) + return { + "rank": rank, + "event_name": row.event_name, + "event_key": row.event_key, + "selected_total_s": seconds(selected_total_us(row, timing)), + "selected_avg_per_event_s": seconds(selected_avg_us(row, timing)), + "selected_avg_per_root_tu_s": seconds(selected_avg_per_root_tu_us(row, timing)), + "selected_max_s": seconds(selected_max_us(row, timing)), + "total_inclusive_s": seconds(row.total_inclusive_us), + "avg_inclusive_per_event_s": seconds( + average_us(row.total_inclusive_us, row.event_count) + ), + "avg_inclusive_per_root_tu_s": seconds( + average_us(row.total_inclusive_us, root_tu_count) + ), + "max_inclusive_s": seconds(row.max_inclusive_us), + "total_exclusive_s": seconds(row.total_exclusive_us), + "avg_exclusive_per_event_s": seconds( + average_us(row.total_exclusive_us, row.event_count) + ), + "avg_exclusive_per_root_tu_s": seconds( + average_us(row.total_exclusive_us, root_tu_count) + ), + "max_exclusive_s": seconds(row.max_exclusive_us), + "event_count": row.event_count, + "trace_count": len(row.trace_paths), + "root_tu_count": root_tu_count, + } + + +def write_csv( + output_csv: Path, + rows: list[EventStats], + timing: str, +) -> None: + output_csv.parent.mkdir(parents=True, exist_ok=True) + with output_csv.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "rank", + "event_name", + "event_key", + "selected_total_s", + "selected_avg_per_event_s", + "selected_avg_per_root_tu_s", + "selected_max_s", + "total_inclusive_s", + "avg_inclusive_per_event_s", + "avg_inclusive_per_root_tu_s", + "max_inclusive_s", + "total_exclusive_s", + "avg_exclusive_per_event_s", + "avg_exclusive_per_root_tu_s", + "max_exclusive_s", + "event_count", + "trace_count", + "root_tu_count", + ], + ) + writer.writeheader() + for rank, row in enumerate(rows, start=1): + writer.writerow(event_stats_csv_row(rank, row, timing)) + + +def comparison_row_dict( + rank: int, row: ComparisonRow, timing: str +) -> dict[str, object]: + stats = row.stats + return { + "rank": rank, + "event_name": stats.event_name, + "event_key": stats.event_key, + "baseline_impact_s": seconds(row.baseline_impact_us), + "current_impact_s": seconds(row.current_impact_us), + "impact_delta_s": seconds(row.impact_delta_us), + "impact_magnitude_s": seconds(row.impact_magnitude_us), + "baseline_selected_s": seconds(row.baseline_metric_us), + "current_selected_s": seconds(row.current_metric_us), + "selected_delta_s": seconds(row.selected_delta_us), + "selected_magnitude_s": seconds(row.selected_magnitude_us), + "baseline_total_inclusive_s": seconds(stats.baseline.total_inclusive_us), + "current_total_inclusive_s": seconds(stats.current.total_inclusive_us), + "baseline_total_exclusive_s": seconds(stats.baseline.total_exclusive_us), + "current_total_exclusive_s": seconds(stats.current.total_exclusive_us), + "baseline_event_count": stats.baseline.event_count, + "current_event_count": stats.current.event_count, + "matched_trace_count": len(stats.matched_trace_paths), + } + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True) + + +def report_side( + name: str, + trace_dir: Path, + repo_root: Path, + output_dir: Path, +) -> ReportSide: + trace_paths = iter_trace_paths(trace_dir) + if not trace_paths: + raise SystemExit(f"no JSON traces found under {trace_dir}") + return ReportSide( + name=name, + trace_paths=trace_paths, + repo_root=repo_root, + output_dir=output_dir, + ) + + +def write_side_report( + side: ReportSide, + config: ReportConfig, +) -> tuple[Path, int]: + stats = collect_stats(side.trace_paths, side.repo_root, config) + rows = sorted_rows(stats, config) if stats else [] + output_csv = default_output_path(side.output_dir, config) + write_csv(output_csv, rows, config.timing) + return output_csv, len(rows) + + +def comparison_rows( + stats: dict[tuple[str, str], ComparisonStats], + config: ReportConfig, + direction: str, +) -> list[ComparisonRow]: + if direction == "worse": + multiplier = 1 + elif direction == "better": + multiplier = -1 + else: + raise ValueError(f"unknown comparison direction: {direction}") + + rows: list[ComparisonRow] = [] + for comparison in stats.values(): + baseline_impact = float(selected_total_us(comparison.baseline, config.timing)) + current_impact = float(selected_total_us(comparison.current, config.timing)) + baseline_metric = selected_metric_us( + comparison.baseline, config.timing, config.sort_by + ) + current_metric = selected_metric_us( + comparison.current, config.timing, config.sort_by + ) + delta = current_impact - baseline_impact + magnitude = multiplier * delta + if magnitude <= config.threshold_us: + continue + rows.append( + ComparisonRow( + comparison, + baseline_impact, + current_impact, + baseline_metric, + current_metric, + magnitude, + ) + ) + + # Python has heapq.nsmallest rather than a C++-style partial_sort. The + # total-impact change is negated here so "smallest" means "largest + # requested aggregate movement across matched traces", while the string + # tie-breakers keep their natural ascending order. + return heapq.nsmallest( + config.top_n, + rows, + key=lambda row: ( + -row.impact_magnitude_us, + row.stats.event_name, + row.stats.event_key, + ), + ) + + +def write_comparison_csv( + output_csv: Path, + rows: list[ComparisonRow], + timing: str, +) -> None: + output_csv.parent.mkdir(parents=True, exist_ok=True) + with output_csv.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "rank", + "event_name", + "event_key", + "baseline_impact_s", + "current_impact_s", + "impact_delta_s", + "impact_magnitude_s", + "baseline_selected_s", + "current_selected_s", + "selected_delta_s", + "selected_magnitude_s", + "baseline_total_inclusive_s", + "current_total_inclusive_s", + "baseline_total_exclusive_s", + "current_total_exclusive_s", + "baseline_event_count", + "current_event_count", + "matched_trace_count", + ], + ) + writer.writeheader() + for rank, row in enumerate(rows, start=1): + writer.writerow(comparison_row_dict(rank, row, timing)) + + +def print_filters() -> None: + for name, spec in sorted(builtin_filters().items()): + print(f"{name:32} {spec.description}") + + +def compile_scope_filter( + pattern: str, parser: argparse.ArgumentParser +) -> re.Pattern[str] | None: + if not pattern: + return None + try: + return re.compile(pattern) + except re.error as e: + parser.error(f"invalid --scope-filter regex: {e}") + + +def report_config( + *, + slice_id: str, + title: str, + filter_name: str, + timing: str, + exclusive_scope: str, + sort_by: str, + top_n: int, + tag: str | None, + threshold_s: float, + scope_filter: re.Pattern[str] | None, +) -> ReportConfig: + spec = resolve_filter(filter_name) + resolved_exclusive_scope = ( + spec.default_exclusive_scope if exclusive_scope == "auto" else exclusive_scope + ) + return ReportConfig( + slice_id=slice_id, + title=title, + spec=spec, + timing=timing, + exclusive_scope=resolved_exclusive_scope, + sort_by=sort_by, + top_n=top_n, + tag=tag, + threshold_us=threshold_s * 1_000_000.0, + scope_filter=scope_filter, + ) + + +def single_slice_request( + args: argparse.Namespace, parser: argparse.ArgumentParser +) -> SliceRequest: + config = report_config( + slice_id=slugify(args.tag or args.filter), + title=args.tag or resolve_filter(args.filter).description, + filter_name=args.filter, + timing=args.timing, + exclusive_scope=args.exclusive_scope, + sort_by=args.sort, + top_n=args.top, + tag=args.tag, + threshold_s=args.threshold, + scope_filter=compile_scope_filter(args.scope_filter, parser), + ) + return SliceRequest(config=config, filter_name=args.filter) + + +def require_slice_field(slice_data: dict[str, Any], field_name: str, path: str) -> Any: + if field_name not in slice_data: + raise ValueError(f"{path}: missing required field '{field_name}'") + return slice_data[field_name] + + +def validate_slice_id(value: Any, path: str) -> str: + if not isinstance(value, str) or not re.fullmatch(r"[a-z0-9][a-z0-9_.-]*", value): + raise ValueError(f"{path}: id must match ^[a-z0-9][a-z0-9_.-]*$") + return value + + +def slice_request_from_json( + slice_data: dict[str, Any], + parser: argparse.ArgumentParser, + *, + path: str, + seen_ids: set[str], +) -> SliceRequest: + if not isinstance(slice_data, dict): + raise ValueError(f"{path}: slice entry must be an object") + + slice_id = validate_slice_id(require_slice_field(slice_data, "id", path), path) + if slice_id in seen_ids: + raise ValueError(f"{path}: duplicate slice id '{slice_id}'") + seen_ids.add(slice_id) + + title = require_slice_field(slice_data, "title", path) + filter_name = require_slice_field(slice_data, "filter", path) + timing = require_slice_field(slice_data, "timing", path) + sort_by = require_slice_field(slice_data, "sort", path) + top_n = require_slice_field(slice_data, "top", path) + threshold = require_slice_field(slice_data, "threshold", path) + exclusive_scope = slice_data.get("exclusive_scope", "auto") + scope_filter_pattern = slice_data.get("scope_filter", DEFAULT_SCOPE_FILTER) + + if not isinstance(title, str) or not title: + raise ValueError(f"{path}: title must be a non-empty string") + if not isinstance(filter_name, str) or not filter_name: + raise ValueError(f"{path}: filter must be a non-empty string") + if timing not in ("inclusive", "exclusive"): + raise ValueError(f"{path}: timing must be 'inclusive' or 'exclusive'") + if sort_by not in ("total", "avg", "avg-root-tu", "max"): + raise ValueError(f"{path}: unsupported sort '{sort_by}'") + if exclusive_scope not in ("auto", "all", "same-filter"): + raise ValueError(f"{path}: unsupported exclusive_scope '{exclusive_scope}'") + if isinstance(top_n, bool) or not isinstance(top_n, int) or top_n <= 0: + raise ValueError(f"{path}: top must be a positive integer") + if ( + isinstance(threshold, bool) + or not isinstance(threshold, (int, float)) + or threshold < 0 + ): + raise ValueError(f"{path}: threshold must be a non-negative number") + if not isinstance(scope_filter_pattern, str): + raise ValueError(f"{path}: scope_filter must be a string") + + children_data = slice_data.get("children", []) + if not isinstance(children_data, list): + raise ValueError(f"{path}: children must be a list") + + config = report_config( + slice_id=slice_id, + title=title, + filter_name=filter_name, + timing=timing, + exclusive_scope=exclusive_scope, + sort_by=sort_by, + top_n=top_n, + tag=None, + threshold_s=float(threshold), + scope_filter=compile_scope_filter(scope_filter_pattern, parser), + ) + children = tuple( + slice_request_from_json( + child, + parser, + path=f"{path}.children[{index}]", + seen_ids=seen_ids, + ) + for index, child in enumerate(children_data) + ) + return SliceRequest(config=config, filter_name=filter_name, children=children) + + +def read_slice_requests( + slices_path: Path, parser: argparse.ArgumentParser +) -> list[SliceRequest]: + try: + with slices_path.open(encoding="utf-8") as f: + payload = json.load(f) + except OSError as e: + parser.error(f"failed to read --slices file: {e}") + except json.JSONDecodeError as e: + parser.error(f"failed to parse --slices JSON: {e}") + + slices_data = payload.get("slices") if isinstance(payload, dict) else payload + if not isinstance(slices_data, list) or not slices_data: + parser.error( + "--slices JSON must be a non-empty list or an object with a non-empty 'slices' list" + ) + + seen_ids: set[str] = set() + try: + return [ + slice_request_from_json( + slice_data, + parser, + path=f"slices[{index}]", + seen_ids=seen_ids, + ) + for index, slice_data in enumerate(slices_data) + ] + except ValueError as e: + parser.error(str(e)) + + +def run_slice_report( + request: SliceRequest, + *, + trace_dir: Path, + baseline_dir: Path | None, + repo_root: Path, + baseline_repo_root: Path, + output_dir: Path, + output_csv: Path | None, + allow_empty: bool, +) -> dict[str, Any]: + config = request.config + slice_output_dir = output_dir + manifest: dict[str, Any] = { + "id": config.slice_id, + "title": config.title, + "filter": request.filter_name, + "filter_label": config.spec.label, + "timing": config.timing, + "exclusive_scope": config.exclusive_scope, + "sort": config.sort_by, + "top": config.top_n, + "threshold_s": config.threshold_us / 1_000_000.0, + "output_dir": slice_output_dir.as_posix(), + "children": [], + } + + if baseline_dir is not None: + comparison_output_dir = slice_output_dir / "comparison" + report_sides = ( + report_side( + "baseline", + baseline_dir, + baseline_repo_root, + slice_output_dir / "baseline", + ), + report_side("current", trace_dir, repo_root, slice_output_dir / "current"), + ) + report_csvs: dict[str, tuple[Path, int]] = {} + for side in report_sides: + report_csvs[side.name] = write_side_report(side, config) + + comparison_stats, matched_trace_count = collect_comparison_stats( + baseline_dir, + trace_dir, + baseline_repo_root, + repo_root, + config, + ) + warnings: list[str] = [] + for side_name, (_, row_count) in report_csvs.items(): + if row_count == 0: + warnings.append( + f"{side_name} report matched no events for this slice; " + f"check filter '{request.filter_name}', scope filtering, " + "and trace format" + ) + if matched_trace_count == 0: + warnings.append( + "baseline and current trace directories have no matching trace files" + ) + elif not comparison_stats: + warnings.append( + "baseline and current traces have no comparable event keys for this slice" + ) + + comparison_manifest: dict[str, Any] = { + "matched_trace_count": matched_trace_count, + } + wrote: list[tuple[Path, int]] = [] + for direction in ("worse", "better"): + rows = comparison_rows(comparison_stats, config, direction) + comparison_csv = comparison_output_path( + comparison_output_dir, + config, + direction, + ) + write_comparison_csv(comparison_csv, rows, config.timing) + row_dicts = [ + comparison_row_dict(rank, row, config.timing) + for rank, row in enumerate(rows, start=1) + ] + comparison_manifest[direction] = { + "csv": comparison_csv.as_posix(), + "row_count": len(rows), + "rows": row_dicts, + } + wrote.append((comparison_csv, len(rows))) + + manifest["reports"] = { + side: {"csv": path.as_posix(), "row_count": row_count} + for side, (path, row_count) in report_csvs.items() + } + manifest["comparison"] = comparison_manifest + if warnings: + manifest["warnings"] = warnings + trace_counts = ", ".join( + f"{len(side.trace_paths)} {side.name} trace(s)" for side in report_sides + ) + print( + f"wrote slice '{config.slice_id}' baseline/current reports and " + f"comparison reports from {trace_counts}, " + f"{matched_trace_count} matched trace file(s):" + ) + for side_name, (path, _) in report_csvs.items(): + print(f" {side_name}: {path}") + for path, row_count in wrote: + print(f" comparison ({row_count} row(s)): {path}") + for warning in warnings: + print(f" warning: {warning}") + else: + side = report_side("current", trace_dir, repo_root, slice_output_dir) + stats = collect_stats(side.trace_paths, side.repo_root, config) + if not stats and not allow_empty: + raise SystemExit( + f"no events matched filter '{request.filter_name}' " + f"in {len(side.trace_paths)} trace(s)" + ) + + rows = sorted_rows(stats, config) if stats else [] + report_csv = ( + output_csv + if output_csv is not None + else default_output_path(slice_output_dir, config) + ) + write_csv(report_csv, rows, config.timing) + manifest["reports"] = { + "current": {"csv": report_csv.as_posix(), "row_count": len(rows)} + } + if not rows: + manifest["warnings"] = [ + f"current report matched no events for filter '{request.filter_name}'" + ] + print( + f"wrote slice '{config.slice_id}' {len(rows)} row(s) " + f"from {len(side.trace_paths)} trace(s) to {report_csv}" + ) + + manifest["children"] = [ + run_slice_report( + child, + trace_dir=trace_dir, + baseline_dir=baseline_dir, + repo_root=repo_root, + baseline_repo_root=baseline_repo_root, + output_dir=output_dir / child.config.slice_id, + output_csv=None, + allow_empty=allow_empty, + ) + for child in request.children + ] + return manifest + + +def main() -> None: + parser = argparse.ArgumentParser( + description=( + "Emit a top-N CSV report for events in NVCC --fdevice-time-trace JSON files." + ) + ) + parser.add_argument( + "trace_dir", + type=Path, + nargs="?", + help="directory containing device-time-trace JSON files", + ) + parser.add_argument( + "-f", + "--filter", + default="file-processing", + help=( + "canonical event filter name or case-insensitive regex over event " + "name/detail (default: file-processing); use --list-filters to see " + "built-in filters" + ), + ) + timing = parser.add_mutually_exclusive_group() + timing.add_argument( + "-i", + "--inclusive", + action="store_const", + const="inclusive", + dest="timing", + help="rank by inclusive event time", + ) + timing.add_argument( + "-e", + "--exclusive", + action="store_const", + const="exclusive", + dest="timing", + help="rank by exclusive event time", + ) + parser.set_defaults(timing="inclusive") + parser.add_argument("-n", "--top", type=int, default=15, help="number of rows") + parser.add_argument( + "--sort", + choices=("total", "avg", "avg-root-tu", "max"), + default="total", + help=( + "sort selected timing by total contribution, average event cost, " + "average per root TU, or max event cost" + ), + ) + parser.add_argument( + "-o", + "--output-dir", + type=Path, + help="output directory (default: /event_reports)", + ) + parser.add_argument( + "--baseline-dir", + type=Path, + help=( + "optional baseline trace directory; writes baseline/current reports " + "and worse/better comparison CSVs under --output-dir" + ), + ) + parser.add_argument( + "--baseline-repo-root", + type=Path, + help=( + "repository root that produced --baseline-dir traces (default: --repo-root)" + ), + ) + parser.add_argument( + "--threshold", + type=float, + default=0.0, + help=( + "comparison-only minimum total-impact change, in seconds, " + "required for worse/better rows (default: 0)" + ), + ) + parser.add_argument( + "--output-csv", + type=Path, + help="exact output CSV path; overrides generated file name inside --output-dir", + ) + parser.add_argument( + "--slices", + type=Path, + help=( + "JSON file describing multiple report slices; writes each slice under " + "--output-dir/ and emits --output-dir/summary.json" + ), + ) + parser.add_argument( + "--exclusive-scope", + choices=("auto", "all", "same-filter"), + default="auto", + help=( + "exclusive timing scope; auto uses same-filter for file-processing " + "and all nested events for other filters" + ), + ) + parser.add_argument( + "--scope-filter", + default=DEFAULT_SCOPE_FILTER, + help=( + "case-sensitive regex for symbol-scope reports; applies to demangled " + "symbols and decoded Itanium-mangled namespace prefixes for symbol-like " + "events only; pass an empty string to disable (default: CCCL top-level " + "namespaces)" + ), + ) + parser.add_argument( + "--repo-root", default=Path(__file__).resolve().parents[2], type=Path + ) + parser.add_argument( + "--tag", + help="optional suffix for the generated output filename", + ) + parser.add_argument( + "--list-filters", + action="store_true", + help="print built-in filters and exit", + ) + args = parser.parse_args() + + if args.list_filters: + print_filters() + return + + if args.trace_dir is None: + parser.error("trace_dir is required unless --list-filters is used") + if args.top <= 0: + parser.error("--top must be positive") + if args.threshold < 0: + parser.error("--threshold must be non-negative") + if args.slices is not None: + ignored_slice_options = ( + (args.filter != parser.get_default("filter"), "--filter"), + (args.timing != parser.get_default("timing"), "--inclusive/--exclusive"), + (args.top != parser.get_default("top"), "--top"), + (args.sort != parser.get_default("sort"), "--sort"), + (args.threshold != parser.get_default("threshold"), "--threshold"), + ( + args.exclusive_scope != parser.get_default("exclusive_scope"), + "--exclusive-scope", + ), + (args.scope_filter != parser.get_default("scope_filter"), "--scope-filter"), + (args.tag is not None, "--tag"), + ) + ignored_names = [name for changed, name in ignored_slice_options if changed] + if ignored_names: + parser.error( + "--slices cannot be combined with single-slice option(s): " + + ", ".join(ignored_names) + ) + if args.baseline_dir is None and args.threshold != 0: + parser.error("--threshold can only be used together with --baseline-dir") + if args.baseline_dir is not None and args.output_csv is not None: + parser.error("--output-csv cannot be used together with --baseline-dir") + if args.slices is not None and args.output_csv is not None: + parser.error("--output-csv cannot be used together with --slices") + + trace_dir = args.trace_dir.resolve(strict=False) + baseline_dir = ( + args.baseline_dir.resolve(strict=False) if args.baseline_dir else None + ) + repo_root = args.repo_root.resolve(strict=False) + baseline_repo_root = ( + args.baseline_repo_root.resolve(strict=False) + if args.baseline_repo_root + else repo_root + ) + output_dir = ( + args.output_dir.resolve(strict=False) + if args.output_dir + else trace_dir / "event_reports" + ) + output_csv = args.output_csv.resolve(strict=False) if args.output_csv else None + multi_slice = args.slices is not None + requests = ( + read_slice_requests(args.slices.resolve(strict=False), parser) + if args.slices is not None + else [single_slice_request(args, parser)] + ) + + manifest = { + "schema_version": 1, + "mode": "comparison" if baseline_dir is not None else "single", + "trace_dir": trace_dir.as_posix(), + "baseline_dir": baseline_dir.as_posix() if baseline_dir else None, + "repo_root": repo_root.as_posix(), + "baseline_repo_root": baseline_repo_root.as_posix(), + "slices": [], + } + + for request in requests: + slice_output_dir = ( + output_dir / request.config.slice_id if multi_slice else output_dir + ) + manifest["slices"].append( + run_slice_report( + request, + trace_dir=trace_dir, + baseline_dir=baseline_dir, + repo_root=repo_root, + baseline_repo_root=baseline_repo_root, + output_dir=slice_output_dir, + output_csv=output_csv, + allow_empty=multi_slice, + ) + ) + + summary_json = output_dir / "summary.json" + write_json(summary_json, manifest) + print(f"wrote summary manifest: {summary_json}") + + +if __name__ == "__main__": + main() diff --git a/cccl_upstream/ci/compile_time/summarize_tus.py b/cccl_upstream/ci/compile_time/summarize_tus.py new file mode 100755 index 00000000..cdca232d --- /dev/null +++ b/cccl_upstream/ci/compile_time/summarize_tus.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import subprocess +from pathlib import Path + +GENERATED_TU_MARKER = "/headers/" +GENERATED_TU_SOURCE_SUFFIXES = (".cu", ".cpp", ".cxx", ".cc", ".c") +PREPROCESSED_TU_SUFFIX = ".cpp4.ii" +PREPROCESSED_TU_SUFFIXES = (".cpp4.ii", ".ii") + + +def strip_generated_tu_suffix(path_text: str) -> str: + for suffix in GENERATED_TU_SOURCE_SUFFIXES: + if path_text.endswith(suffix): + return path_text[: -len(suffix)] + return path_text + + +def generated_tu_input(tu: Path) -> str: + parts = tu.as_posix().split(GENERATED_TU_MARKER, 1) + if len(parts) != 2: + return tu.as_posix() + + rel = parts[1].split("/", 1) + if len(rel) != 2: + return tu.as_posix() + + return strip_generated_tu_suffix(rel[1]) + + +def find_preprocessed_tus(build_dir: Path) -> list[Path]: + return sorted( + { + path + for suffix in PREPROCESSED_TU_SUFFIXES + for path in build_dir.glob(f"**/headers/**/*{suffix}") + } + ) + + +def tu_source_for_preprocessed_tu(pp_path: Path) -> Path: + pp_text = pp_path.as_posix() + for suffix in PREPROCESSED_TU_SUFFIXES: + if pp_text.endswith(suffix): + return Path(pp_text[: -len(suffix)]) + return pp_path.with_suffix("") + + +def run_cloc(preprocessed_tus: list[Path], processes: int) -> dict[str, int]: + if not preprocessed_tus: + return {} + + command = [ + "cloc", + "--csv", + "--by-file", + "--skip-uniqueness", + "--processes", + str(processes), + "--force-lang=C++,ii", + *[path.as_posix() for path in preprocessed_tus], + ] + result = subprocess.run(command, check=True, capture_output=True, text=True) + + loc_by_file: dict[str, int] = {} + reader = csv.reader(result.stdout.splitlines()) + for row in reader: + if len(row) < 5 or row[1] == "filename": + continue + try: + loc_by_file[row[1]] = int(row[4]) + except ValueError: + continue + return loc_by_file + + +def write_summary( + output_csv: Path, + preprocessed_tus: list[Path], + loc_by_file: dict[str, int], +) -> None: + output_csv.parent.mkdir(parents=True, exist_ok=True) + with output_csv.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "tu_input", + "transitive_loc", + "tu_source", + "preprocessed_tu", + ], + ) + writer.writeheader() + for pp_path in preprocessed_tus: + tu_path = tu_source_for_preprocessed_tu(pp_path) + writer.writerow( + { + "tu_input": generated_tu_input(tu_path), + "transitive_loc": loc_by_file.get(pp_path.as_posix(), 0), + "tu_source": tu_path.as_posix(), + "preprocessed_tu": pp_path.as_posix(), + } + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Summarize generated TU inputs and preprocessed LOC." + ) + parser.add_argument("--build-dir", required=True, type=Path) + parser.add_argument("--output-csv", required=True, type=Path) + parser.add_argument( + "--cloc-processes", + type=int, + default=0, + help="cloc process count; 0 uses nproc --all --ignore=2 when available", + ) + args = parser.parse_args() + + build_dir = args.build_dir.resolve(strict=False) + preprocessed_tus = find_preprocessed_tus(build_dir) + if not preprocessed_tus: + raise SystemExit(f"no preprocessed generated TUs found under {build_dir}") + + processes = args.cloc_processes + if processes <= 0: + try: + processes = int( + subprocess.check_output( + ["nproc", "--all", "--ignore=2"], text=True + ).strip() + ) + except (subprocess.SubprocessError, ValueError): + processes = 1 + + write_summary( + output_csv=args.output_csv, + preprocessed_tus=preprocessed_tus, + loc_by_file=run_cloc(preprocessed_tus, processes), + ) + print(f"wrote {len(preprocessed_tus)} generated TU row(s) to {args.output_csv}") + + +if __name__ == "__main__": + main() diff --git a/cccl_upstream/ci/compile_time/test_summarize_events.py b/cccl_upstream/ci/compile_time/test_summarize_events.py new file mode 100644 index 00000000..a6093a83 --- /dev/null +++ b/cccl_upstream/ci/compile_time/test_summarize_events.py @@ -0,0 +1,1731 @@ +#!/usr/bin/env python3 + +import csv +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from ci.compile_time import summarize_tus + +REPO_ROOT = Path(__file__).resolve().parents[2] +SUMMARY_SCRIPT = REPO_ROOT / "ci" / "compile_time" / "summarize_events.py" +PREPARE_SCRIPT = REPO_ROOT / "ci" / "compile_time" / "prepare_traces.py" +PARSE_MATRIX_SCRIPT = REPO_ROOT / "ci" / "compile_time" / "parse_matrix.py" +RENDER_COMMENT_SCRIPT = REPO_ROOT / "ci" / "compile_time" / "render_pr_comment.py" +WRAPPER_SCRIPT = REPO_ROOT / "ci" / "build_compile_time_bench.sh" +PULL_REQUEST_WORKFLOW = ( + REPO_ROOT / ".github" / "workflows" / "ci-workflow-pull-request.yml" +) + + +def csv_rows(path: Path) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +class TraceBuilder: + def __init__(self, root: Path): + self.root = root + + def project_detail(self, rel: str) -> str: + return (self.root / rel).as_posix() + + def event( + self, + name: str, + detail: str, + ts: int, + dur: int, + *, + pid: int = 1, + tid: int = 1, + ) -> dict: + return { + "ph": "X", + "pid": pid, + "tid": tid, + "name": name, + "ts": ts, + "dur": dur, + "args": {"detail": detail}, + } + + def write_trace(self, path: Path, events: list[dict], input_name: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps( + { + "traceEvents": events, + "otherData": { + "inputFiles": [f"/generated/headers/target/{input_name}.cu"] + }, + } + ), + encoding="utf-8", + ) + + +class SummarizeEventsBaselineCompareTest(unittest.TestCase): + def setUp(self) -> None: + self.tempdir = tempfile.TemporaryDirectory() + self.work = Path(self.tempdir.name) + self.traces = TraceBuilder(REPO_ROOT) + self.wrapper_infix = f"compile-time-test-{self.work.name}" + self.wrapper_preset = "all-dev" + self.wrapper_build_dir = ( + REPO_ROOT / "build" / self.wrapper_infix / self.wrapper_preset + ) + + def tearDown(self) -> None: + shutil.rmtree(REPO_ROOT / "build" / self.wrapper_infix, ignore_errors=True) + self.tempdir.cleanup() + + def comparison_csv(self, output_dir: Path, name: str) -> Path: + return output_dir / "comparison" / name + + def wrapper_trace_dir(self) -> Path: + return self.wrapper_build_dir / "compile_time" / "raw_traces" + + def wrapper_output_dir(self) -> Path: + return self.wrapper_build_dir / "compile_time" / "event_reports" + + def assert_empty_csv(self, path: Path) -> None: + self.assertTrue(path.exists()) + self.assertEqual(csv_rows(path), []) + + def run_summary( + self, + current: Path, + baseline: Path, + output_dir: Path, + *args: str, + baseline_repo_root: Path | None = None, + ) -> subprocess.CompletedProcess[str]: + command = [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + current.as_posix(), + "--baseline-dir", + baseline.as_posix(), + "-o", + output_dir.as_posix(), + ] + if baseline_repo_root is not None: + command.extend(["--baseline-repo-root", baseline_repo_root.as_posix()]) + command.extend(args) + + return subprocess.run( + command, + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def run_wrapper( + self, + *args: str, + no_tu_csv: bool = True, + common_args: tuple[str, ...] = (), + ) -> subprocess.CompletedProcess[str]: + command = [ + "bash", + WRAPPER_SCRIPT.as_posix(), + "-skip-configure", + "-skip-build", + "-no-prepare-perfetto", + *common_args, + ] + if no_tu_csv: + command.append("-no-tu-csv") + command.extend(["--", *args]) + env = os.environ.copy() + env["CCCL_BUILD_INFIX"] = self.wrapper_infix + return subprocess.run( + command, + cwd=REPO_ROOT, + env=env, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def test_comparison_outputs_and_unmatched_children(self) -> None: + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + parent = self.traces.project_detail("libcudacxx/include/cuda/std/parent.h") + matched_child = self.traces.project_detail( + "libcudacxx/include/cuda/std/matched_child.h" + ) + better = self.traces.project_detail("libcudacxx/include/cuda/std/better.h") + baseline_only_child = self.traces.project_detail( + "libcudacxx/include/cuda/std/baseline_only_child.h" + ) + baseline_only = self.traces.project_detail( + "libcudacxx/include/cuda/std/only_baseline.h" + ) + current_only = self.traces.project_detail( + "libcudacxx/include/cuda/std/only_current.h" + ) + + self.traces.write_trace( + baseline / "target" / "match.json", + [ + self.traces.event("Parent", parent, 0, 100), + self.traces.event("Child", matched_child, 10, 30), + self.traces.event("Child", baseline_only_child, 50, 20), + self.traces.event("Better", better, 200, 70), + self.traces.event("OnlyBaseline", baseline_only, 300, 20), + ], + "match", + ) + self.traces.write_trace( + current / "target" / "match.json", + [ + self.traces.event("Parent", parent, 0, 150), + self.traces.event("Child", matched_child, 10, 40), + self.traces.event("Better", better, 200, 40), + self.traces.event("OnlyCurrent", current_only, 300, 90), + ], + "match", + ) + self.traces.write_trace( + baseline / "target" / "baseline_only_trace.json", + [self.traces.event("Parent", parent, 0, 999)], + "baseline_only_trace", + ) + self.traces.write_trace( + current / "target" / "current_only_trace.json", + [self.traces.event("Parent", parent, 0, 999)], + "current_only_trace", + ) + + self.run_summary( + current, + baseline, + output, + "-f", + "all", + "-e", + "--sort", + "total", + "-n", + "10", + "--tag", + "synthetic", + ) + + worse = csv_rows( + self.comparison_csv( + output, "top-10-all-exclusive-all-by-total-worse-synthetic.csv" + ) + ) + better_rows = csv_rows( + self.comparison_csv( + output, "top-10-all-exclusive-all-by-total-better-synthetic.csv" + ) + ) + + parent_row = next(row for row in worse if row["event_name"] == "Parent") + self.assertEqual(parent_row["baseline_selected_s"], "0.000070") + self.assertEqual(parent_row["current_selected_s"], "0.000110") + self.assertEqual(parent_row["impact_magnitude_s"], "0.000040") + + better_row = next(row for row in better_rows if row["event_name"] == "Better") + self.assertEqual(better_row["baseline_selected_s"], "0.000070") + self.assertEqual(better_row["current_selected_s"], "0.000040") + self.assertEqual(better_row["impact_magnitude_s"], "0.000030") + + all_comparison_keys = {row["event_key"] for row in worse + better_rows} + self.assertFalse(any("only_baseline" in key for key in all_comparison_keys)) + self.assertFalse(any("only_current" in key for key in all_comparison_keys)) + + def test_comparison_threshold_filters_small_changes(self) -> None: + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + small = self.traces.project_detail("libcudacxx/include/cuda/std/small.h") + large = self.traces.project_detail("libcudacxx/include/cuda/std/large.h") + self.traces.write_trace( + baseline / "target" / "match.json", + [ + self.traces.event("Same", small, 0, 10), + self.traces.event("Same", large, 100, 10), + ], + "match", + ) + self.traces.write_trace( + current / "target" / "match.json", + [ + self.traces.event("Same", small, 0, 12), + self.traces.event("Same", large, 100, 20), + ], + "match", + ) + + self.run_summary( + current, + baseline, + output, + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "10", + "--threshold", + "0.000005", + "--tag", + "threshold", + ) + + worse = csv_rows( + self.comparison_csv( + output, "top-10-all-inclusive-by-total-worse-threshold.csv" + ) + ) + self.assertEqual(len(worse), 1) + self.assertTrue(worse[0]["event_key"].endswith("large.h")) + self.assertEqual(worse[0]["impact_magnitude_s"], "0.000010") + + def test_comparison_ranks_by_total_impact_not_selected_metric(self) -> None: + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + repeated = self.traces.project_detail("libcudacxx/include/cuda/std/repeated.h") + one_trace = self.traces.project_detail( + "libcudacxx/include/cuda/std/one_trace.h" + ) + self.traces.write_trace( + baseline / "target" / "first.json", + [ + self.traces.event("Same", repeated, 0, 10), + self.traces.event("Same", one_trace, 100, 10), + ], + "first", + ) + self.traces.write_trace( + current / "target" / "first.json", + [ + self.traces.event("Same", repeated, 0, 16), + self.traces.event("Same", one_trace, 100, 20), + ], + "first", + ) + self.traces.write_trace( + baseline / "target" / "second.json", + [self.traces.event("Same", repeated, 0, 10)], + "second", + ) + self.traces.write_trace( + current / "target" / "second.json", + [self.traces.event("Same", repeated, 0, 16)], + "second", + ) + + self.run_summary( + current, + baseline, + output, + "-f", + "all", + "-i", + "--sort", + "max", + "-n", + "10", + "--tag", + "impact", + ) + + worse = csv_rows( + self.comparison_csv(output, "top-10-all-inclusive-by-max-worse-impact.csv") + ) + self.assertTrue(worse[0]["event_key"].endswith("repeated.h")) + self.assertEqual(worse[0]["impact_magnitude_s"], "0.000012") + self.assertEqual(worse[0]["selected_magnitude_s"], "0.000006") + + def test_threshold_requires_comparison_mode(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + traces / "target" / "same.json", + [self.traces.event("Same", same, 0, 10)], + "same", + ) + + completed = subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "--threshold", + "0.000001", + ], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertNotEqual(completed.returncode, 0) + self.assertIn( + "--threshold can only be used together with --baseline-dir", + completed.stderr, + ) + + def test_file_processing_same_filter_keeps_unmatched_child_in_parent_cost( + self, + ) -> None: + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + parent = self.traces.project_detail("libcudacxx/include/cuda/std/parent.h") + matched_child = self.traces.project_detail( + "libcudacxx/include/cuda/std/matched_child.h" + ) + unmatched_child = self.traces.project_detail( + "libcudacxx/include/cuda/std/baseline_only_child.h" + ) + + self.traces.write_trace( + baseline / "target" / "same.json", + [ + self.traces.event("Processing Header File", parent, 0, 100), + self.traces.event("Processing Header File", matched_child, 10, 30), + self.traces.event("Processing Header File", unmatched_child, 50, 20), + ], + "same", + ) + self.traces.write_trace( + current / "target" / "same.json", + [ + self.traces.event("Processing Header File", parent, 0, 130), + self.traces.event("Processing Header File", matched_child, 10, 40), + ], + "same", + ) + + self.run_summary( + current, + baseline, + output, + "-f", + "file-processing", + "-e", + "--sort", + "total", + "-n", + "5", + "--tag", + "file-processing", + ) + worse = csv_rows( + self.comparison_csv( + output, + "top-5-file-processing-exclusive-same-filter-by-total-worse-file-processing.csv", + ) + ) + parent_row = next(row for row in worse if row["event_key"].endswith("parent.h")) + self.assertEqual(parent_row["baseline_selected_s"], "0.000070") + self.assertEqual(parent_row["current_selected_s"], "0.000090") + + def test_same_filter_exclusive_uses_direct_children_only(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + parent = self.traces.project_detail("libcudacxx/include/cuda/std/parent.h") + nested = self.traces.project_detail("libcudacxx/include/cuda/std/nested.h") + + self.traces.write_trace( + traces / "target" / "direct.json", + [ + self.traces.event("Processing Header File", parent, 0, 100), + self.traces.event("Scanning Function Body", "not-a-header", 10, 80), + self.traces.event("Processing Header File", nested, 20, 10), + ], + "direct", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "file-processing", + "-e", + "--sort", + "total", + "-n", + "5", + "--tag", + "direct", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows( + output / "top-5-file-processing-exclusive-same-filter-by-total-direct.csv" + ) + parent_row = next(row for row in rows if row["event_key"].endswith("parent.h")) + self.assertEqual(parent_row["selected_total_s"], "0.000100") + + def test_empty_comparisons_still_write_csvs(self) -> None: + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + foo = self.traces.project_detail("libcudacxx/include/cuda/std/foo.h") + bar = self.traces.project_detail("libcudacxx/include/cuda/std/bar.h") + self.traces.write_trace( + baseline / "target" / "same.json", + [self.traces.event("Foo", foo, 0, 10)], + "same", + ) + self.traces.write_trace( + current / "target" / "same.json", + [self.traces.event("Bar", bar, 0, 20)], + "same", + ) + self.run_summary( + current, + baseline, + output, + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "empty", + ) + + self.assert_empty_csv( + self.comparison_csv(output, "top-5-all-inclusive-by-total-worse-empty.csv") + ) + self.assert_empty_csv( + self.comparison_csv(output, "top-5-all-inclusive-by-total-better-empty.csv") + ) + with (output / "summary.json").open(encoding="utf-8") as f: + manifest = json.load(f) + self.assertIn( + "no comparable event keys", + " ".join(manifest["slices"][0]["warnings"]), + ) + + def test_wrapper_forwarding_writes_current_report(self) -> None: + current = self.wrapper_trace_dir() + output = self.wrapper_output_dir() + + same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + current / "target" / "current_name.json", + [self.traces.event("Same", same, 0, 100)], + "current_name", + ) + + self.run_wrapper( + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "wrapper", + common_args=("-arch", "70"), + ) + + self.assertTrue((output / "top-5-all-inclusive-by-total-wrapper.csv").exists()) + + def test_comparison_matches_paths_from_distinct_repo_roots(self) -> None: + baseline_repo = self.work / "baseline-src" + baseline = self.work / "baseline" + current = self.work / "current" + output = self.work / "reports" + + baseline_traces = TraceBuilder(baseline_repo) + baseline_same = baseline_traces.project_detail( + "libcudacxx/include/cuda/std/same.h" + ) + current_same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + current / "target" / "same.json", + [self.traces.event("Same", current_same, 0, 12)], + "same", + ) + baseline_traces.write_trace( + baseline / "target" / "same.json", + [baseline_traces.event("Same", baseline_same, 0, 10)], + "same", + ) + + self.run_summary( + current, + baseline, + output, + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "5", + baseline_repo_root=baseline_repo, + ) + + worse = csv_rows( + self.comparison_csv(output, "top-5-all-inclusive-by-total-worse.csv") + ) + self.assertEqual(worse[0]["event_key"], "libcudacxx/include/cuda/std/same.h") + self.assertEqual(worse[0]["impact_magnitude_s"], "0.000002") + + def test_wrapper_uses_computed_event_output_dir(self) -> None: + current = self.wrapper_trace_dir() + wrapper_default = self.wrapper_output_dir() + explicit_output = self.work / "explicit-output" + + same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + current / "target" / "same.json", + [self.traces.event("Same", same, 0, 12)], + "same", + ) + + self.run_wrapper( + f"--output-dir={explicit_output.as_posix()}", + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "equals-output", + ) + + self.assertTrue( + ( + wrapper_default / "top-5-all-inclusive-by-total-equals-output.csv" + ).exists() + ) + self.assertFalse( + ( + explicit_output / "top-5-all-inclusive-by-total-equals-output.csv" + ).exists() + ) + + def test_sort_by_average_per_root_tu(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + one_tu = self.traces.project_detail("libcudacxx/include/cuda/std/one_tu.h") + two_tus = self.traces.project_detail("libcudacxx/include/cuda/std/two_tus.h") + self.traces.write_trace( + traces / "target" / "first.json", + [ + self.traces.event("Same", one_tu, 0, 80), + self.traces.event("Same", two_tus, 100, 60), + ], + "first", + ) + self.traces.write_trace( + traces / "target" / "second.json", + [self.traces.event("Same", two_tus, 0, 40)], + "second", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "all", + "-i", + "--sort", + "avg-root-tu", + "-n", + "2", + "--tag", + "avg-root-tu", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows(output / "top-2-all-inclusive-by-avg-root-tu-avg-root-tu.csv") + self.assertTrue(rows[0]["event_key"].endswith("one_tu.h")) + self.assertEqual(rows[0]["selected_avg_per_root_tu_s"], "0.000080") + self.assertTrue(rows[1]["event_key"].endswith("two_tus.h")) + self.assertEqual(rows[1]["selected_avg_per_root_tu_s"], "0.000050") + + def test_host_compiler_filter_matches_host_phase_events(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "host.json", + [ + self.traces.event( + "g++ (preprocessing 1)", "g++ (preprocessing 1)", 0, 10 + ), + self.traces.event("gcc (compiling)", "gcc (compiling)", 20, 30), + self.traces.event("CUDA C++ Front-End", "frontend", 60, 40), + ], + "host", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "host-compiler", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "host", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows(output / "top-5-host-compiler-inclusive-by-total-host.csv") + self.assertEqual( + {row["event_name"] for row in rows}, + {"g++ (preprocessing 1)", "gcc (compiling)"}, + ) + + def test_scope_filter_defaults_to_cccl_demangled_symbols(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "symbols.json", + [ + self.traces.event( + "Scanning Function Body", + "std::basic_string_view::size() const noexcept", + 0, + 100, + ), + self.traces.event( + "Scanning Function Body", + "cuda::std::__4::basic_string_view::size() const noexcept", + 200, + 80, + ), + self.traces.event( + "Scanning Function Body", + 'cuda::std::literals::operator ""sv(const char *, unsigned long)', + 400, + 60, + ), + ], + "symbols", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "scanning-function-body", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "scope-default", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows( + output / "top-5-scanning-function-body-inclusive-by-total-scope-default.csv" + ) + self.assertEqual(len(rows), 2) + self.assertIn("cuda::std", rows[0]["event_key"]) + self.assertIn("operator", rows[1]["event_key"]) + + def test_empty_scope_filter_disables_symbol_scope_filtering(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "symbols.json", + [ + self.traces.event( + "Scanning Function Body", + "std::basic_string_view::size() const noexcept", + 0, + 100, + ), + self.traces.event( + "Scanning Function Body", + "cuda::std::__4::basic_string_view::size() const noexcept", + 200, + 80, + ), + ], + "symbols", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "scanning-function-body", + "-i", + "--sort", + "total", + "-n", + "5", + "--scope-filter", + "", + "--tag", + "scope-disabled", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows( + output + / "top-5-scanning-function-body-inclusive-by-total-scope-disabled.csv" + ) + self.assertEqual(len(rows), 2) + self.assertIn("std::basic_string_view", rows[0]["event_key"]) + self.assertIn("cuda::std", rows[1]["event_key"]) + + def test_scope_filter_uses_reported_template_symbol_scope(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "templates.json", + [ + self.traces.event( + "Instantiating Template Function", + ( + "nvtx3::v1::domain::get " + "[nvtx3::v1::domain::get()]" + ), + 0, + 100, + ), + self.traces.event( + "Instantiating Template Function", + "cub::detail::load [cub::detail::load()]", + 200, + 80, + ), + ], + "templates", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "template-instantiation", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "template-scope", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows( + output + / "top-5-template-instantiation-inclusive-by-total-template-scope.csv" + ) + self.assertEqual(len(rows), 1) + self.assertIn("cub::detail::load", rows[0]["event_key"]) + + def test_scope_filter_matches_mangled_cccl_namespaces(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "mangled.json", + [ + self.traces.event( + "Generating Function IR", + "_ZN4cuda3std3__43fooEv", + 0, + 100, + ), + self.traces.event( + "Generating Function IR", + "_ZN6thrust6detail3barEv", + 200, + 80, + ), + self.traces.event( + "Generating Function IR", + "_ZNSt6vectorIiE4sizeEv", + 400, + 200, + ), + ], + "mangled", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "code-generation", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "mangled-scope", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rows = csv_rows( + output / "top-5-code-generation-inclusive-by-total-mangled-scope.csv" + ) + self.assertEqual( + {row["event_key"] for row in rows}, + { + "_ZN4cuda3std3__43fooEv", + "_ZN6thrust6detail3barEv", + }, + ) + + def test_total_compilation_filter_uses_trace_wall_span(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + + self.traces.write_trace( + traces / "target" / "total.json", + [ + self.traces.event( + "g++ (preprocessing 1)", "g++ (preprocessing 1)", 10, 20 + ), + self.traces.event("CUDA C++ Front-End", "frontend", 50, 40), + ], + "total", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "total-compilation", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "total", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + total_rows = csv_rows( + output / "top-5-total-compilation-inclusive-by-total-total.csv" + ) + self.assertEqual(len(total_rows), 1) + self.assertEqual(total_rows[0]["event_name"], "Total Compilation Time") + self.assertEqual(total_rows[0]["selected_total_s"], "0.000080") + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "-f", + "all", + "-i", + "--sort", + "total", + "-n", + "5", + "--tag", + "all-no-total", + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + all_rows = csv_rows(output / "top-5-all-inclusive-by-total-all-no-total.csv") + self.assertFalse( + any(row["event_name"] == "Total Compilation Time" for row in all_rows) + ) + + def test_same_dir_comparison_is_empty(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + traces / "target" / "same.json", + [self.traces.event("Same", same, 0, 10)], + "same", + ) + + self.run_summary( + traces, + traces, + output, + "-f", + "all", + "-i", + "--sort", + "max", + "-n", + "5", + "--tag", + "same", + ) + self.assert_empty_csv( + self.comparison_csv(output, "top-5-all-inclusive-by-max-worse-same.csv") + ) + self.assert_empty_csv( + self.comparison_csv(output, "top-5-all-inclusive-by-max-better-same.csv") + ) + + def test_multi_slice_writes_manifest_and_allows_empty_slice(self) -> None: + traces = self.work / "traces" + output = self.work / "reports" + slices = self.work / "slices.json" + same = self.traces.project_detail("libcudacxx/include/cuda/std/same.h") + self.traces.write_trace( + traces / "target" / "same.json", + [self.traces.event("Same", same, 0, 10)], + "same", + ) + slices.write_text( + json.dumps( + { + "slices": [ + { + "id": "all-events", + "title": "All events", + "filter": "all", + "timing": "inclusive", + "sort": "total", + "top": 5, + "threshold": 0, + }, + { + "id": "empty-events", + "title": "Empty events", + "filter": "does-not-match", + "timing": "inclusive", + "sort": "total", + "top": 5, + "threshold": 0, + }, + ] + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + SUMMARY_SCRIPT.as_posix(), + traces.as_posix(), + "-o", + output.as_posix(), + "--slices", + slices.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + with (output / "summary.json").open(encoding="utf-8") as f: + manifest = json.load(f) + + self.assertEqual( + [item["id"] for item in manifest["slices"]], ["all-events", "empty-events"] + ) + self.assertEqual(manifest["slices"][0]["reports"]["current"]["row_count"], 1) + self.assertEqual(manifest["slices"][1]["reports"]["current"]["row_count"], 0) + self.assertTrue( + ( + output + / "empty-events" + / "top-5-regex-does-not-match-inclusive-by-total.csv" + ).exists() + ) + + +class CompileTimeMatrixAndCommentTest(unittest.TestCase): + def setUp(self) -> None: + self.tempdir = tempfile.TemporaryDirectory() + self.work = Path(self.tempdir.name) + + def tearDown(self) -> None: + self.tempdir.cleanup() + + def test_parse_matrix_disabled_when_section_missing(self) -> None: + matrix = self.work / "matrix.yaml" + matrix.write_text("workflows: {}\n", encoding="utf-8") + + completed = subprocess.run( + [ + sys.executable, + PARSE_MATRIX_SCRIPT.as_posix(), + matrix.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertEqual(json.loads(completed.stdout), {"include": []}) + + def test_parse_matrix_valid_config(self) -> None: + matrix = self.work / "matrix.yaml" + matrix.write_text( + """ +compile_time: + pull_request: + - id: public-headers + name: Public headers + gpu: rtx2080 + launch_args: "--cuda 13.3 --host gcc13" + baseline_ref: origin/main + preset: all-dev + targets: [cub.headers.base] + args: "-arch native" + slices: + - id: total-compilation + title: TU total compilation + filter: total-compilation + timing: inclusive + sort: total + top: 15 + threshold: 0.001 +""", + encoding="utf-8", + ) + + completed = subprocess.run( + [ + sys.executable, + PARSE_MATRIX_SCRIPT.as_posix(), + matrix.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + include = json.loads(completed.stdout)["include"] + self.assertEqual(len(include), 1) + self.assertEqual( + include[0]["comment_header"], "compile-time-bench-public-headers" + ) + self.assertEqual(json.loads(include[0]["targets_json"]), ["cub.headers.base"]) + self.assertEqual( + json.loads(include[0]["slices_json"])["slices"][0]["id"], + "total-compilation", + ) + + def test_parse_matrix_rejects_duplicate_slice_ids(self) -> None: + matrix = self.work / "matrix.yaml" + matrix.write_text( + """ +compile_time: + pull_request: + - id: public-headers + name: Public headers + gpu: rtx2080 + launch_args: "--cuda 13.3 --host gcc13" + baseline_ref: origin/main + preset: all-dev + targets: [cub.headers.base] + slices: + - id: repeated + title: First + filter: all + timing: inclusive + sort: total + top: 15 + threshold: 0 + - id: repeated + title: Second + filter: all + timing: inclusive + sort: total + top: 15 + threshold: 0 +""", + encoding="utf-8", + ) + + completed = subprocess.run( + [ + sys.executable, + PARSE_MATRIX_SCRIPT.as_posix(), + matrix.as_posix(), + ], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertNotEqual(completed.returncode, 0) + self.assertIn("duplicate slice id", completed.stderr) + + def test_parse_matrix_rejects_bool_numeric_fields(self) -> None: + matrix = self.work / "matrix.yaml" + matrix.write_text( + """ +compile_time: + pull_request: + - id: public-headers + name: Public headers + gpu: rtx2080 + launch_args: "--cuda 13.3 --host gcc13" + baseline_ref: origin/main + preset: all-dev + targets: [cub.headers.base] + artifact_retention_days: true + slices: + - id: total-compilation + title: TU total compilation + filter: total-compilation + timing: inclusive + sort: total + top: true + threshold: false +""", + encoding="utf-8", + ) + + completed = subprocess.run( + [ + sys.executable, + PARSE_MATRIX_SCRIPT.as_posix(), + matrix.as_posix(), + ], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertNotEqual(completed.returncode, 0) + self.assertIn("positive integer", completed.stderr) + + def test_pull_request_workflow_supports_compile_time_bench_skip_tag(self) -> None: + workflow = PULL_REQUEST_WORKFLOW.read_text(encoding="utf-8") + + self.assertIn("[skip-compile-time-bench]", workflow) + self.assertIn("compile_time_enabled=false", workflow) + self.assertIn('compile_time_matrix={"include":[]}', workflow) + + def test_render_comment_omits_empty_sections_and_splits_directions(self) -> None: + summary = self.work / "summary.json" + config = self.work / "config.json" + output = self.work / "comment.md" + summary.write_text( + json.dumps( + { + "slices": [ + { + "id": "nonempty", + "title": "Nonempty", + "filter": "all", + "timing": "inclusive", + "sort": "total", + "comparison": { + "worse": { + "rows": [ + { + "rank": 1, + "event_name": "Scanning Function Body", + "event_key": "cuda::std::__4::same(int)", + "baseline_selected_s": "0.000001", + "current_selected_s": "0.000003", + "impact_magnitude_s": "0.000010", + "selected_delta_s": "0.000002", + "matched_trace_count": 1, + } + ] + }, + "better": {"rows": []}, + }, + "children": [ + { + "id": "empty-child", + "title": "Empty child", + "filter": "all", + "timing": "inclusive", + "sort": "total", + "comparison": { + "worse": {"rows": []}, + "better": {"rows": []}, + }, + "children": [], + } + ], + } + ] + } + ), + encoding="utf-8", + ) + config.write_text( + json.dumps( + { + "id": "public-headers", + "name": "Public headers", + "baseline_ref": "origin/main", + "preset": "all-dev", + "targets": ["cub.headers.base"], + "gpu": "rtx2080", + "launch_args": "--cuda 13.3 --host gcc13", + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + RENDER_COMMENT_SCRIPT.as_posix(), + "--summary", + summary.as_posix(), + "--config", + config.as_posix(), + "--artifacts-url", + "https://example.test/artifacts", + "-o", + output.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rendered = output.read_text(encoding="utf-8") + self.assertIn("", rendered) + self.assertIn("Regressions", rendered) + self.assertIn("Regression impact", rendered) + self.assertIn( + "Scanning Function Body: `cuda::std::__4::same(int)`", + rendered, + ) + self.assertIn("0.000010", rendered) + self.assertNotIn("Improvements", rendered) + self.assertNotIn("Empty child", rendered) + self.assertIn("https://example.test/artifacts", rendered) + + def test_render_comment_reports_empty_slice_warnings(self) -> None: + summary = self.work / "summary.json" + config = self.work / "config.json" + output = self.work / "comment.md" + summary.write_text( + json.dumps( + { + "slices": [ + { + "id": "empty", + "title": "Empty slice", + "filter": "typo-filter", + "timing": "inclusive", + "sort": "total", + "warnings": [ + "baseline report matched no events for this slice" + ], + "comparison": { + "worse": {"rows": []}, + "better": {"rows": []}, + }, + "children": [], + } + ] + } + ), + encoding="utf-8", + ) + config.write_text( + json.dumps( + { + "id": "public-headers", + "name": "Public headers", + "baseline_ref": "origin/main", + "preset": "all-dev", + "targets": ["cub.headers.base"], + "gpu": "rtx2080", + "launch_args": "--cuda 13.3 --host gcc13", + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + RENDER_COMMENT_SCRIPT.as_posix(), + "--summary", + summary.as_posix(), + "--config", + config.as_posix(), + "--artifacts-url", + "https://example.test/artifacts", + "-o", + output.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rendered = output.read_text(encoding="utf-8") + self.assertIn("1 warning(s)", rendered) + self.assertIn("Empty slice — Warnings", rendered) + self.assertIn("baseline report matched no events", rendered) + self.assertNotIn( + "No compile-time benchmark changes exceeded the configured thresholds.", + rendered, + ) + + def test_render_comment_separates_top_level_slice_sections(self) -> None: + summary = self.work / "summary.json" + config = self.work / "config.json" + output = self.work / "comment.md" + row = { + "rank": 1, + "event_name": "Same", + "event_key": "cuda/std/same", + "baseline_selected_s": "0.000001", + "current_selected_s": "0.000003", + "impact_magnitude_s": "0.000010", + "selected_delta_s": "0.000002", + "matched_trace_count": 1, + } + summary.write_text( + json.dumps( + { + "slices": [ + { + "id": "first", + "title": "First", + "filter": "all", + "timing": "inclusive", + "sort": "total", + "comparison": { + "worse": {"rows": [row]}, + "better": {"rows": []}, + }, + "children": [], + }, + { + "id": "second", + "title": "Second", + "filter": "all", + "timing": "inclusive", + "sort": "total", + "comparison": { + "worse": {"rows": [row]}, + "better": {"rows": []}, + }, + "children": [], + }, + ] + } + ), + encoding="utf-8", + ) + config.write_text( + json.dumps( + { + "id": "public-headers", + "name": "Public headers", + "baseline_ref": "origin/main", + "preset": "all-dev", + "targets": ["cub.headers.base"], + "gpu": "rtx2080", + "launch_args": "--cuda 13.3 --host gcc13", + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + RENDER_COMMENT_SCRIPT.as_posix(), + "--summary", + summary.as_posix(), + "--config", + config.as_posix(), + "--artifacts-url", + "https://example.test/artifacts", + "-o", + output.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + rendered = output.read_text(encoding="utf-8") + self.assertIn("
\n\n### Second", rendered) + self.assertNotIn("\n### Second", rendered) + + +class PrepareTracesTest(unittest.TestCase): + def setUp(self) -> None: + self.tempdir = tempfile.TemporaryDirectory() + self.work = Path(self.tempdir.name) + + def tearDown(self) -> None: + self.tempdir.cleanup() + + def test_promotes_project_details_into_perfetto_event_names(self) -> None: + input_dir = self.work / "raw" + output_dir = self.work / "perfetto" + trace_path = input_dir / "target" / "trace.json" + trace_path.parent.mkdir(parents=True, exist_ok=True) + trace_path.write_text( + json.dumps( + { + "traceEvents": [ + { + "ph": "X", + "name": "Processing Header File", + "args": { + "detail": ( + REPO_ROOT + / "libcudacxx/include/cuda/std/string_view" + ).as_posix() + }, + }, + { + "ph": "X", + "name": "Other Event", + "args": {"detail": "not promoted"}, + }, + ] + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + PREPARE_SCRIPT.as_posix(), + "--input", + input_dir.as_posix(), + "--output", + output_dir.as_posix(), + "--repo-root", + REPO_ROOT.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + output_trace = output_dir / "target" / "trace.perfetto.json" + with output_trace.open(encoding="utf-8") as f: + events = json.load(f)["traceEvents"] + + self.assertEqual( + events[0]["name"], "Processing Header File: cuda/std/string_view" + ) + self.assertEqual(events[0]["args"]["original_name"], "Processing Header File") + self.assertEqual(events[1]["name"], "Other Event") + + def test_single_file_input_accepts_output_directory(self) -> None: + input_trace = self.work / "trace.json" + output_dir = self.work / "perfetto" + input_trace.write_text( + json.dumps( + { + "traceEvents": [ + { + "ph": "X", + "name": "Processing Header File", + "args": {"detail": "cuda/std/string_view"}, + } + ] + } + ), + encoding="utf-8", + ) + + subprocess.run( + [ + sys.executable, + PREPARE_SCRIPT.as_posix(), + "--input", + input_trace.as_posix(), + "--output", + output_dir.as_posix(), + "--repo-root", + REPO_ROOT.as_posix(), + ], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertTrue((output_dir / "trace.perfetto.json").exists()) + + +class SummarizeTusTest(unittest.TestCase): + def test_generated_tu_input(self) -> None: + build_dir = Path("/tmp/build") + pp_path = ( + build_dir + / "libcudacxx/test" + / "headers" + / "libcudacxx.test.public_headers" + / "cuda/std/string_view.cpp4.ii" + ) + + self.assertEqual( + summarize_tus.generated_tu_input(pp_path.with_name("string_view.cpp")), + "cuda/std/string_view", + ) + + def test_finds_nvcc_and_clang_preprocessed_tus(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + build_dir = Path(tmp) + header_dir = build_dir / "target" / "headers" / "generated" + header_dir.mkdir(parents=True) + nvcc_tu = header_dir / "cuda_std_span.cpp4.ii" + clang_tu = header_dir / "cuda_std_string_view.ii" + nvcc_tu.write_text("", encoding="utf-8") + clang_tu.write_text("", encoding="utf-8") + + self.assertEqual( + summarize_tus.find_preprocessed_tus(build_dir), + [nvcc_tu, clang_tu], + ) + self.assertEqual( + summarize_tus.tu_source_for_preprocessed_tu(nvcc_tu), + header_dir / "cuda_std_span", + ) + self.assertEqual( + summarize_tus.tu_source_for_preprocessed_tu(clang_tu), + header_dir / "cuda_std_string_view", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/cccl_upstream/ci/compute-sanitizer-suppressions.xml b/cccl_upstream/ci/compute-sanitizer-suppressions.xml new file mode 100644 index 00000000..37aa0fe5 --- /dev/null +++ b/cccl_upstream/ci/compute-sanitizer-suppressions.xml @@ -0,0 +1,453 @@ + + + + + Initcheck + + Uninitialized __global__ memory read of size 2 bytes + 2 + + + ThreadLoad + + + + UnrolledThreadLoadImpl + + + UnrolledThreadLoad + + + ThreadLoad + + + ThreadLoad + + + + + .*libcuda.so.* + + + libcudart_static.* + + + cudaLaunchKernel + + + .*cub::.*::DeviceReduce.*.*thrust::.*find_if.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 16 + + + error + + .*/libcuda.so.* + + + libcudart_static_.* + + + libcudart_static_.* + + + cudaMemcpyAsync + + + void C_A_T_C_H_T_E_M_P_L_A_T_E_T_E_S_T_F_U_N_C_.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 16 + + + error + + .*/libcuda.so.* + + + libcudart_static_.* + + + libcudart_static_.* + + + cudaMemcpyAsync + + + void CATCH2_INTERNAL_TEMPLATE_TEST.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 16 + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + + + .*thrust::.*(equal|operator==|mismatch|find_if).* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 16 + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + + + bool binary_equal.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 16 + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + + + + main + .*cub.*test.namespace_wrapped + + + + + + InitcheckApiError + + Host API uninitialized memory access + 16 + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + + + + .*cub.*device_segmented_sort.* + + + + + + InitcheckApiError + + Host API uninitialized memory access + 16 + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + + + + .*cub.*device_run_length_encode.* + + + + + + Analysis + Error + + Race condition + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + + + + Analysis + Error + + Race condition + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + + + + Analysis + Error + + Race condition + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + Write + + ScatterToStriped + + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 32 + + + error + + .*libcuda.so.* + + + libcudart_static_.* + + + libcudart_static_.* + + + cudaMemcpyAsync + + + thrust.*vector_base.*ConstantInputIterator.* + + + void test_iterator.*ConstantInputIterator.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 32 + + + error + + .*/libcuda.so.* + + + libcudart_static_.* + + + libcudart_static_.* + + + cudaMemcpyAsync + + + thrust.*vector_base.*TransformInputIterator.* + + + void test_iterator.*TransformInputIterator.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + 64 + + + error + + .*/libcuda.so.* + + + libcudart_static_.* + + + libcudart_static_.* + + + cudaMemcpyAsync + + + thrust.*vector_base.*TransformInputIterator.* + + + void test_iterator.*TransformInputIterator.* + + + + + + InitcheckApiError + Error + + Host API uninitialized memory access + + + error + + .*libcuda.so.* + + + libcudart_static.* + + + libcudart_static.* + + + cudaMemcpyAsync + .*cub.*device_(segmented_|)reduce.* + + + + diff --git a/cccl_upstream/ci/generate_version.sh b/cccl_upstream/ci/generate_version.sh new file mode 100755 index 00000000..cffe3785 --- /dev/null +++ b/cccl_upstream/ci/generate_version.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Generate a version number string using metadata from git or JSON. +# Use the PyPi package versioning convention for pre-release or +# post release patches. + +# Set some defaults for variables. +CCCL_BRANCH="${CCCL_BRANCH:-dev}" +PACKAGE_VERSION_PREFIX="${PACKAGE_VERSION_PREFIX:-}" + +GIT_DESCRIBE_TAG=$(git describe --tags --match "v[0-9]*" --abbrev=0) +GIT_DESCRIBE_NUMBER=$(git rev-list "${GIT_DESCRIBE_TAG}"..HEAD --count) + +JSON_VERSION=$(jq -r .full /workspace/cccl-version.json) + +# Generate a suffix depending on release or dev branch. +PACKAGE_VERSION_SUFFIX="" +if [[ "${GIT_DESCRIBE_NUMBER}" != "0" ]]; then + if [[ ${CCCL_BRANCH} == "dev" ]]; then + PACKAGE_VERSION_SUFFIX=".dev${GIT_DESCRIBE_NUMBER}" + else + PACKAGE_VERSION_SUFFIX=".post${GIT_DESCRIBE_NUMBER}" + fi +fi + +# If using Git metadata this could generate it from the last tag. +# VERSION="${GIT_DESCRIBE_TAG#v}.dev${GIT_DESCRIBE_NUMBER}" + +# Generate the version using a combination of JSON and git commit number. +VERSION="${PACKAGE_VERSION_PREFIX}${JSON_VERSION}${PACKAGE_VERSION_SUFFIX}" + +echo -n "${VERSION}" diff --git a/cccl_upstream/ci/inspect_changes.py b/cccl_upstream/ci/inspect_changes.py new file mode 100755 index 00000000..cfd013ae --- /dev/null +++ b/cccl_upstream/ci/inspect_changes.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python3 +"""Identify dirty CCCL subprojects between two commits or explicit path lists.""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from collections import deque +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Tuple + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +CONFIG_PATH = REPO_ROOT / "ci" / "project_files_and_dependencies.yaml" +CORE_PROJECT_KEY = "core" + + +class SummaryWriter: + """Utility for duplicating output to stdout and an optional summary file.""" + + def __init__(self, path: Optional[Path]): + self._handle = path.open("a", encoding="utf-8") if path else None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + if self._handle: + self._handle.close() + + def log(self, line: str = "") -> None: + print(line) + if self._handle: + self._handle.write(line + "\n") + + +@dataclass(frozen=True) +class ProjectConfig: + """Per-project configuration derived from the YAML file.""" + + key: str + name: str + matrix_project: Optional[str] + include_regexes: Tuple[str, ...] + exclude_regexes: Tuple[str, ...] + exclude_project_files: Tuple[str, ...] + lite_dependencies: Tuple[str, ...] + full_dependencies: Tuple[str, ...] + transitive_lite_dependencies: Tuple[str, ...] = () + + +@dataclass(frozen=True) +class Config: + """Aggregated configuration for change detection.""" + + projects: Dict[str, ProjectConfig] + project_keys: Tuple[str, ...] + ignore_regexes: Tuple[str, ...] + + def project(self, key: str) -> ProjectConfig: + return self.projects[key] + + +def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + """Parse CLI arguments for determining dirty files.""" + parser = argparse.ArgumentParser( + description="Identify which CCCL projects require rebuilds between two commits." + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--refs", + nargs=2, + metavar=("BASE", "HEAD"), + help="Compare two refs using 'git diff --name-only' to determine dirty files", + ) + group.add_argument( + "--file", + metavar="PATH", + help="Read dirty file paths (one per line) from PATH", + ) + group.add_argument( + "--stdin", + action="store_true", + help="Read dirty file paths (one per line) from stdin", + ) + parser.add_argument( + "--summary", + metavar="PATH", + default=None, + help="Optional path to write a markdown summary table", + ) + return parser.parse_args(argv) + + +def load_config(path: Path) -> Config: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) + projects_raw = raw.get("projects") + if not isinstance(projects_raw, dict) or not projects_raw: + raise SystemExit(f"No projects defined in {path}") + + def to_tuple(value: Optional[Sequence[str] | str]) -> Tuple[str, ...]: + if value is None: + return tuple() + if isinstance(value, (list, tuple)): + return tuple(value) + return (value,) + + project_keys: List[str] = list(projects_raw.keys()) + projects: Dict[str, ProjectConfig] = {} + for key in project_keys: + entry = projects_raw.get(key) or {} + name = entry.get("name", key) + matrix_project = entry.get("matrix_project") + include_regexes = to_tuple(entry.get("include_regexes", [])) + exclude_regexes = to_tuple(entry.get("exclude_regexes", [])) + exclude_project_files = to_tuple(entry.get("exclude_project_files")) + lite_dependencies = to_tuple(entry.get("lite_dependencies")) + full_dependencies = to_tuple(entry.get("full_dependencies")) + + if key != CORE_PROJECT_KEY and not include_regexes: + raise SystemExit( + f"Project '{key}' must define at least one include_regex in {path}" + ) + + projects[key] = ProjectConfig( + key=key, + name=name, + matrix_project=matrix_project, + include_regexes=include_regexes, + exclude_regexes=exclude_regexes, + exclude_project_files=exclude_project_files, + lite_dependencies=lite_dependencies, + full_dependencies=full_dependencies, + transitive_lite_dependencies=tuple(), + ) + + if CORE_PROJECT_KEY not in projects: + raise SystemExit(f"Project configuration must define '{CORE_PROJECT_KEY}'") + + dependency_graph = build_dependency_graph_raw(projects) + transitive_map = compute_transitive_dependencies( + project_keys, projects, dependency_graph + ) + + for key, transitive in transitive_map.items(): + projects[key] = replace(projects[key], transitive_lite_dependencies=transitive) + + ignore_regexes = tuple(raw.get("ignore_regexes", ())) + + return Config( + projects=projects, + project_keys=tuple(project_keys), + ignore_regexes=ignore_regexes, + ) + + +def run_git(args: Sequence[str], *, capture_output: bool = True) -> str: + """Run a git command in the repo and optionally capture stdout.""" + result = subprocess.run( + ["git", *args], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.PIPE if capture_output else None, + ) + if capture_output: + return result.stdout.strip() + return "" + + +def try_rev_parse(ref: str) -> str: + try: + return run_git(["rev-parse", ref]) + except subprocess.CalledProcessError: + return ref + + +def ensure_fetched(ref: str) -> None: + """Fetch a ref from origin ensuring availability for merge-base.""" + run_git(["fetch", "origin", ref, "-q"], capture_output=False) + + +def repo_is_shallow() -> bool: + """Return True when the repository has a shallow history.""" + output = run_git(["rev-parse", "--is-shallow-repository"]) + return output.strip().lower() == "true" + + +def indent(text: str, prefix: str) -> str: + """Indent multi-line text for readable logging blocks.""" + return "\n".join(f"{prefix}{line}" for line in text.splitlines()) + + +def anchor_regex(pattern: str) -> re.Pattern[str]: + """Anchor a path regex to the repository root.""" + anchored = pattern if pattern.startswith("^") else f"^{pattern}" + return re.compile(anchored) + + +def compile_patterns(patterns: Sequence[str]) -> Tuple[re.Pattern[str], ...]: + """Compile a list of regex strings into anchored patterns.""" + return tuple(anchor_regex(pattern) for pattern in patterns) + + +def matches_any(patterns: Sequence[re.Pattern[str]], path: str) -> bool: + """Return True when any compiled regex matches the given path.""" + return any(pattern.search(path) for pattern in patterns) + + +def build_dependency_graph_raw( + projects: Dict[str, ProjectConfig], +) -> Dict[str, List[Tuple[str, str]]]: + """Return mapping of project -> [(dependency, type)].""" + graph: Dict[str, List[Tuple[str, str]]] = {} + for key, project in projects.items(): + edges: List[Tuple[str, str]] = [] + edges.extend((dep, "full") for dep in project.full_dependencies) + edges.extend((dep, "lite") for dep in project.lite_dependencies) + graph[key] = edges + return graph + + +def compute_transitive_dependencies( + project_keys: Sequence[str], + projects: Dict[str, ProjectConfig], + graph: Dict[str, List[Tuple[str, str]]], +) -> Dict[str, Tuple[str, ...]]: + """Return a dictionary of transitive dependencies for each project.""" + result: Dict[str, Tuple[str, ...]] = {} + + for key in project_keys: + visited: set[str] = set() + queue: deque[str] = deque(dep for dep, _ in graph.get(key, [])) + + while queue: + dep = queue.popleft() + if dep == key or dep in visited: + continue + visited.add(dep) + queue.extend(child for child, _ in graph.get(dep, [])) + + project_cfg = projects[key] + direct_full = set(project_cfg.full_dependencies) + direct_lite = set(project_cfg.lite_dependencies) + transitive = [ + dep + for dep in project_keys + if dep in visited and dep not in direct_full and dep not in direct_lite + ] + + result[key] = tuple(transitive) + + return result + + +def project_dirty_files( + project: ProjectConfig, dirty_files: Sequence[str] +) -> List[str]: + """Collect dirty files that belong to the given project. Does not apply project exclusions yet.""" + include_patterns = compile_patterns(project.include_regexes) + exclude_patterns = compile_patterns(project.exclude_regexes) + + if not include_patterns: + return [] + + included = [path for path in dirty_files if matches_any(include_patterns, path)] + + if exclude_patterns: + included = [ + path for path in included if not matches_any(exclude_patterns, path) + ] + + return included + + +def build_project_dirty_map( + config: Config, dirty_files: Sequence[str] +) -> Dict[str, List[str]]: + """Compute per-project dirty file lists, including a residual `core` list.""" + project_files: Dict[str, List[str]] = {} + + # First gather matches for every non-core project. Files may belong to multiple projects. + for key in config.project_keys: + if key == CORE_PROJECT_KEY: + continue + files = project_dirty_files(config.project(key), dirty_files) + project_files[key] = files + + # Remove files that are owned by other projects when requested. + for key in config.project_keys: + if key == CORE_PROJECT_KEY: + continue + project = config.project(key) + if not project.exclude_project_files: + continue + excluded_sets = [ + set(project_files.get(other, [])) for other in project.exclude_project_files + ] + if not excluded_sets: + continue + exclusions = set().union(*excluded_sets) + project_files[key] = [ + path for path in project_files[key] if path not in exclusions + ] + + matched_paths: set[str] = set() + for key in config.project_keys: + if key == CORE_PROJECT_KEY: + continue + matched_paths.update(project_files.get(key, [])) + + core_files = [path for path in dirty_files if path not in matched_paths] + project_files[CORE_PROJECT_KEY] = core_files + return project_files + + +def write_output(key: str, value: str) -> None: + """Emit a GitHub Actions output key/value pair.""" + line = f"{key}={value}" + print(line) + output_path = os.environ.get("GITHUB_OUTPUT") + if output_path: + with open(output_path, "a", encoding="utf-8") as handle: + handle.write(f"{line}\n") + + +def collect_dirty_files( + args: argparse.Namespace, config: Config +) -> Tuple[List[str], List[str]]: + """Return normalized dirty file paths and the subset filtered as ignored.""" + if args.refs: + if repo_is_shallow(): + run_git(["fetch", "origin", "--unshallow", "-q"], capture_output=False) + + base_ref = try_rev_parse(args.refs[0]) + head_ref = try_rev_parse(args.refs[1]) + + ensure_fetched(base_ref) + ensure_fetched(head_ref) + + base_sha = run_git(["merge-base", base_ref, head_ref]) + head_sha = run_git(["rev-parse", head_ref]) + + print(f"Base SHA: {base_sha}") + base_log = run_git(["log", "--oneline", "-1", base_sha]) + if base_log: + print(indent(base_log, " ")) + print(f"HEAD SHA: {head_sha}") + head_log = run_git(["log", "--oneline", "-1", head_sha]) + if head_log: + print(indent(head_log, " ")) + + dirty_files = run_git(["diff", "--name-only", base_sha, head_sha]).splitlines() + elif args.file: + print(f"Dirty files provided via file: {args.file}") + with open(args.file, "r", encoding="utf-8") as handle: + dirty_files = [line.strip() for line in handle if line.strip()] + else: # args.stdin + print("Dirty files provided via stdin") + dirty_files = [line.strip() for line in sys.stdin if line.strip()] + + print() + + dirty_files = [path for path in dirty_files if path] + + ignore_patterns = compile_patterns(config.ignore_regexes) + if ignore_patterns: + kept: List[str] = [] + ignored: List[str] = [] + for path in dirty_files: + if matches_any(ignore_patterns, path): + ignored.append(path) + else: + kept.append(path) + return kept, ignored + + return dirty_files, [] + + +def format_bullet_list(lines: Sequence[str], indent: str = " ") -> List[str]: + """Convert each string into a markdown bullet line with the given indent.""" + return [f"{indent}- {line}" for line in lines] + + +def build_reverse_dependency_graph(config: Config) -> Dict[str, List[Tuple[str, bool]]]: + """Return mapping of dependency -> (dependent, requires_full_rebuild).""" + reverse: Dict[str, List[Tuple[str, bool]]] = {} + + for project in config.projects.values(): + for dep in project.full_dependencies: + reverse.setdefault(dep, []).append((project.key, True)) + for dep in project.lite_dependencies: + reverse.setdefault(dep, []).append((project.key, False)) + for dep in project.transitive_lite_dependencies: + reverse.setdefault(dep, []).append((project.key, False)) + + return reverse + + +def propagate_dirty_projects( + config: Config, + initial_full: Sequence[str], # Projects with dirty files + reverse_graph: Dict[ + str, List[Tuple[str, bool]] + ], # dependency -> [(dependent, requires_full_rebuild)] +) -> Tuple[set[str], set[str]]: # (full_set, lite_set) + """Propagate rebuild requirements through the reverse dependency graph.""" + full_set: set[str] = set(initial_full) + lite_set: set[str] = set() + # BFS queue of (project_key, depth) + queue: deque[Tuple[str, int]] = deque((key, 0) for key in initial_full) + seen: set[Tuple[str, int]] = set(queue) + + while queue: + current, depth = queue.popleft() + for dependent, edge_full in reverse_graph.get(current, []): + propagate_full = edge_full and depth == 0 + + if propagate_full: + if dependent in full_set: + continue + if dependent in lite_set: + lite_set.remove(dependent) + full_set.add(dependent) + print( + "- Upstream dependency change detected (full rebuild): " + f"'{dependent}' ({config.project(dependent).name}) depends on dirty project '{current}'" + ) + else: + if dependent in full_set or dependent in lite_set: + continue + lite_set.add(dependent) + print( + "- Upstream dependency change detected (lite rebuild): " + f"'{dependent}' ({config.project(dependent).name}) depends on dirty project '{current}'" + ) + + key = (dependent, depth + 1) + if key not in seen: + seen.add(key) + queue.append(key) + + return full_set, lite_set + + +def log_dependency_overview(config: Config) -> None: + """Pretty-print the dependency data for each project.""" + print("Project Dependency Overview:") + for key in config.project_keys: + project = config.project(key) + header = f"- {project.name} (key={key}" + if project.matrix_project: + header += f" matrix_project={project.matrix_project}" + header += ")" + print(header) + + if project.full_dependencies: + print(" - direct full deps:") + for line in format_bullet_list(project.full_dependencies, indent=" "): + print(line) + if project.lite_dependencies: + print(" - direct lite deps:") + for line in format_bullet_list(project.lite_dependencies, indent=" "): + print(line) + if project.transitive_lite_dependencies: + print(" - transitive deps:") + for line in format_bullet_list( + project.transitive_lite_dependencies, indent=" " + ): + print(line) + print() + + +def build_dirty_sections( + config: Config, + project_dirty_map: Dict[str, List[str]], + ignored_files: Sequence[str], +) -> List[Tuple[str, List[str]]]: + """Create (heading, files) tuples for non-empty dirty buckets.""" + sections: List[Tuple[str, List[str]]] = [] + for key in config.project_keys: + if key == CORE_PROJECT_KEY: + continue + files = project_dirty_map.get(key, []) + if files: + sections.append((f"{config.project(key).name} ({key})", list(files))) + + core_files = project_dirty_map.get(CORE_PROJECT_KEY, []) + if core_files: + sections.append( + ( + f"{config.project(CORE_PROJECT_KEY).name} ({CORE_PROJECT_KEY})", + list(core_files), + ) + ) + + if ignored_files: + sections.append(("Ignored Files", list(ignored_files))) + + return sections + + +def log_dirty_files( + combined_dirty: Sequence[str], + sections: Sequence[Tuple[str, Sequence[str]]], +) -> None: + """Emit dirty-file information in markdown list form.""" + if sections: + print("Dirty files by project:") + for idx, (heading, files) in enumerate(sections): + print(f"{heading}:") + for line in format_bullet_list(files): + print(line) + if idx != len(sections) - 1: + print() + print() + + print("All dirty files:") + if combined_dirty: + for line in format_bullet_list(combined_dirty): + print(line) + else: + print(" - (none)") + print() + + +def determine_rebuild_sets( + config: Config, + project_dirty_map: Dict[str, List[str]], # key -> dirty files + reverse_graph: Dict[ + str, List[Tuple[str, bool]] + ], # dependency -> [(dependent, requires_full_rebuild)] +) -> Tuple[ + Dict[str, str], set[str], set[str] +]: # (project_statuses, full_set, lite_set) + """Return project statuses plus the full/lite rebuild sets.""" + core_dirty_files = project_dirty_map[CORE_PROJECT_KEY] + + if core_dirty_files: + project_statuses: Dict[str, str] = {key: "Dirty" for key in config.project_keys} + return project_statuses, set(config.project_keys), set() + + initially_dirty = [ + key + for key in config.project_keys + if key != CORE_PROJECT_KEY and project_dirty_map.get(key) + ] + for key in initially_dirty: + print(f"- Changes detected in subproject '{key}' ({config.project(key).name})") + + full_set, lite_set = propagate_dirty_projects( + config, + initially_dirty, + reverse_graph, + ) + + project_statuses: Dict[str, str] = {} + for key in config.project_keys: + if key in full_set: + project_statuses[key] = "Dirty" + elif key in lite_set: + project_statuses[key] = "Dirty Deps" + else: + project_statuses[key] = "Clean" + + return project_statuses, full_set, lite_set + + +def compute_outputs( + config: Config, + full_set: set[str], + lite_set: set[str], +) -> Tuple[List[str], List[str]]: # (FULL_BUILD, LITE_BUILD) matrix_project lists + """Convert rebuild sets into ordered matrix project lists.""" + full_output = [ + config.project(key).matrix_project + for key in config.project_keys + if key in full_set and config.project(key).matrix_project + ] + lite_output = [ + config.project(key).matrix_project + for key in config.project_keys + if key in lite_set and config.project(key).matrix_project + ] + return full_output, lite_output + + +def emit_outputs(full_output: Sequence[str], lite_output: Sequence[str]) -> None: + """Write FULL_BUILD/LITE_BUILD strings to stdout and GitHub outputs.""" + print("Github Action Outputs:") + write_output("FULL_BUILD", " ".join(full_output)) + write_output("LITE_BUILD", " ".join(lite_output)) + print() + + +def write_project_summary( + config: Config, + project_statuses: Dict[str, str], + writer: SummaryWriter, +) -> None: + """Render the status table inside the summary section.""" + name_width = max(len(config.project(key).name) for key in config.project_keys) + writer.log(f"| {'Project':<{name_width}} | Status |") + writer.log(f"|{'-' * (name_width + 2)}|------------|") + for key in config.project_keys: + project = config.project(key) + status = project_statuses.get(key, "Unknown?") + writer.log(f"| {project.name:<{name_width}} | {status:<10} |") + writer.log() + + +def write_summary_dirty_sections( + combined_dirty: Sequence[str], + sections: Sequence[Tuple[str, Sequence[str]]], + writer: SummaryWriter, +) -> None: + """Render dirty-file details inside the summary section.""" + writer.log("

👉 Dirty Files

") + if sections: + for idx, (heading, files) in enumerate(sections): + writer.log() + writer.log(f"{heading}:") + for line in format_bullet_list(files): + writer.log(line) + + writer.log() + if combined_dirty: + writer.log("All dirty files:") + for line in format_bullet_list(combined_dirty): + writer.log(line) + + writer.log() + writer.log("
") + + +def main(argv: Optional[Sequence[str]] = None) -> int: + """Entrypoint used by the GitHub Action wrapper.""" + args = parse_args(argv) + config = load_config(CONFIG_PATH) + + summary_path = Path(args.summary) if args.summary else None + + dirty_files, ignored_files = collect_dirty_files(args, config) + + project_dirty_map = build_project_dirty_map(config, dirty_files) + reverse_graph = build_reverse_dependency_graph(config) + + print(f"Subprojects: {' '.join(config.project_keys)}") + print() + + log_dependency_overview(config) + + combined_dirty = dirty_files + ignored_files + sections = build_dirty_sections(config, project_dirty_map, ignored_files) + log_dirty_files(combined_dirty, sections) + + print("Checking for changes...") + + project_statuses, full_set, lite_set = determine_rebuild_sets( + config, + project_dirty_map, + reverse_graph, + ) + + full_output, lite_output = compute_outputs(config, full_set, lite_set) + + print() + emit_outputs(full_output, lite_output) + + with SummaryWriter(summary_path) as summary_writer: + print("::group::Project Change Summary") + summary_writer.log( + "

👃 Inspect Project Changes

" + ) + summary_writer.log() + write_project_summary(config, project_statuses, summary_writer) + write_summary_dirty_sections(combined_dirty, sections, summary_writer) + summary_writer.log("
") + print("::endgroup::") + + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/cccl_upstream/ci/install_cccl.sh b/cccl_upstream/ci/install_cccl.sh new file mode 100755 index 00000000..41eddadd --- /dev/null +++ b/cccl_upstream/ci/install_cccl.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +set -eo pipefail + +target_dir=$(realpath "$1") +mkdir -p "$target_dir" + +# Move script to the root directory of the project +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +source ./pretty_printing.sh + +# Check if the correct number of arguments has been provided +function usage { + echo "Usage: $0 [OPTIONS] dir" + echo + echo "Installs CCCL to the provided directory" + + echo "Options:" + echo " -v/-verbose: Enable shell echo for debugging" + echo + echo "Examples:" + echo " $ $0 ~/my/prefix" + exit 1 +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --verbose) VERBOSE=true; ;; + -v) VERBOSE=true; ;; + *) break ;; + esac + shift +done + +if [[ -n "$VERBOSE" ]]; then + set -x +fi + +# Move to cccl/ dir +pushd ".." > /dev/null +GROUP_NAME="🛠️ CMake Configure CCCL - Install" +run_command "$GROUP_NAME" cmake -G "Unix Makefiles" --preset install -DCMAKE_INSTALL_PREFIX="${target_dir}" +status=$? + +GROUP_NAME="🏗️ Install CCCL" +run_command "$GROUP_NAME" cmake --build --preset install --target install +status=$? +popd > /dev/null diff --git a/cccl_upstream/ci/install_packaging.sh b/cccl_upstream/ci/install_packaging.sh new file mode 100755 index 00000000..d8b257ef --- /dev/null +++ b/cccl_upstream/ci/install_packaging.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Time limit for installation steps (seconds) +readonly install_time_limit=20 + +ci_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "$ci_dir/pretty_printing.sh" + +cd "$ci_dir/.." + +# Prefixes for installations +prefix_default=$(mktemp -d /tmp/cccl-default-XXXX) +prefix_preset=$(mktemp -d /tmp/cccl-preset-XXXX) +prefix_script=$(mktemp -d /tmp/cccl-script-XXXX) +prefix_preset_unstable=$(mktemp -d /tmp/cccl-preset-unstable-XXXX) +prefix_preset_unstable_only=$(mktemp -d /tmp/cccl-preset-unstable-only-XXXX) + +# Default configure + install +default_start_time=$SECONDS +run_command "Configure default" \ + cmake -S . -B build/default -DCMAKE_INSTALL_PREFIX="$prefix_default" +run_command "Install default" \ + cmake --build build/default --target install +default_time=$((SECONDS - default_start_time)) + +# Preset configure + install +preset_start_time=$SECONDS +CCCL_BUILD_INFIX=preset \ +run_command "Configure preset" \ + cmake --preset install -DCMAKE_INSTALL_PREFIX="$prefix_preset" +CCCL_BUILD_INFIX=preset \ +run_command "Install preset" \ + cmake --build --preset install --target install +preset_time=$((SECONDS - preset_start_time)) + +# Preset configure + install-unstable +preset_unstable_start_time=$SECONDS +CCCL_BUILD_INFIX=preset_unstable \ +run_command "Configure preset-unstable" \ + cmake --preset install-unstable -DCMAKE_INSTALL_PREFIX="$prefix_preset_unstable" +CCCL_BUILD_INFIX=preset_unstable \ +run_command "Install preset-unstable" \ + cmake --build build/preset_unstable/install-unstable --target install +preset_unstable_time=$((SECONDS - preset_unstable_start_time)) + +# Preset configure + install-unstable-only +preset_unstable_only_start_time=$SECONDS +CCCL_BUILD_INFIX=preset_unstable_only \ +run_command "Configure preset-unstable-only" \ + cmake --preset install-unstable-only -DCMAKE_INSTALL_PREFIX="$prefix_preset_unstable_only" +CCCL_BUILD_INFIX=preset_unstable_only \ +run_command "Install preset-unstable-only" \ + cmake --build build/preset_unstable_only/install-unstable-only \ + --target install +preset_unstable_only_time=$((SECONDS - preset_unstable_only_start_time)) + +# Script install +script_start_time=$SECONDS +CCCL_BUILD_INFIX=script \ +run_command "Install script" \ + ci/install_cccl.sh "$prefix_script" +script_time=$((SECONDS - script_start_time)) + +# Compare directories +if ! diff -ruN "$prefix_default" "$prefix_preset" > /tmp/diff_default_preset.log; then + cat /tmp/diff_default_preset.log + echo -e "\e[0;31mDefault and preset installations differ\e[0m" + exit 1 +fi +if ! diff -ruN "$prefix_default" "$prefix_script" > /tmp/diff_default_script.log; then + cat /tmp/diff_default_script.log + echo -e "\e[0;31mDefault and script installations differ\e[0m" + exit 1 +fi + +# Verify unstable-only contains only cudax headers and cmake config +if find "$prefix_preset_unstable_only" -type f | \ + sed "s|$prefix_preset_unstable_only/||" | \ + grep -vE '^include/cuda/experimental/|^lib/cmake/+cudax/|^lib/cmake/+cccl/' | \ + grep -q .; then + echo -e "\e[0;31minstall-unstable-only contained unexpected files\e[0m" >&2 + exit 1 +fi + +# Verify install-unstable is union of install and install-unstable-only +prefix_union=$(mktemp -d /tmp/cccl-union-XXXX) +cp -a "$prefix_default/." "$prefix_union/" +cp -a "$prefix_preset_unstable_only/." "$prefix_union/" +if ! diff -ruN "$prefix_union" "$prefix_preset_unstable" > /tmp/diff_union_unstable.log; then + echo -e "\e[0;31mUnion of install and install-unstable-only did not match install-unstable\e[0m" >&2 + cat /tmp/diff_union_unstable.log + exit 1 +fi + +print_time_summary + +# Verify times +violations=() +(( default_time > install_time_limit )) && violations+=("default_time=${default_time}s") +(( preset_time > install_time_limit )) && violations+=("preset_time=${preset_time}s") +(( preset_unstable_time > install_time_limit )) && violations+=("preset_unstable_time=${preset_unstable_time}s") +(( preset_unstable_only_time > install_time_limit )) && violations+=("preset_unstable_only_time=${preset_unstable_only_time}s") +(( script_time > install_time_limit )) && violations+=("script_time=${script_time}s") + +if (( ${#violations[@]} > 0 )); then + echo -e "\e[0;31mInstallation time limit (${install_time_limit}s) exceeded by: ${violations[*]}\e[0m" >&2 + exit 1 +fi + +# Clean up +rm -rf "$prefix_default" "$prefix_preset" "$prefix_script" \ + "$prefix_preset_unstable" "$prefix_preset_unstable_only" \ + "$prefix_union" + +echo -e "\e[0;32mAll installation tests passed successfully.\e[0m" diff --git a/cccl_upstream/ci/matrix.yaml b/cccl_upstream/ci/matrix.yaml new file mode 100644 index 00000000..5ea7a70b --- /dev/null +++ b/cccl_upstream/ci/matrix.yaml @@ -0,0 +1,816 @@ +compile_time: + pull_request: + - id: public-headers-gcc13 + name: Public headers compile-time bench + gpu: rtx2080 + launch_args: "--cuda 13.3 --host gcc13" + baseline_ref: origin/main + preset: all-dev + targets: + - cub.headers.base + - thrust.cpp.cuda.headers.base + - libcudacxx.test.public_headers + args: "-arch native" + slices: + - id: total-compilation + title: TU total compilation + filter: total-compilation + timing: inclusive + sort: total + top: 15 + threshold: 3.0 + - id: file-processing + title: Direct file processing + filter: file-processing + timing: exclusive + sort: total + top: 15 + threshold: 0.2 + - id: function-bodies + title: Function-body parsing + filter: scanning-function-body + timing: inclusive + sort: total + top: 15 + threshold: 0.25 + - id: template-instantiation + title: Template instantiation + filter: template-instantiation + timing: inclusive + sort: total + top: 15 + threshold: 0.2 + +workflows: + # If any jobs appear here, they will be executed instead of `pull_request' for PRs. + # This is useful for limiting resource usage when a full matrix is not needed. + # The branch protection checks will fail when using this override workflow. + # + # Example: + # override: + # # Full project build: slow, expensive + # - { jobs: ['test'], project: 'thrust', std: 17, ctk: '12.X', cxx: ['gcc12', 'clang16'] } + # + # # Build / run targeted tests: faster turnaround, less runner usage. + # # Use project 'target'. + # # args are passed to ci/util/build_and_test_targets.sh. See that script for available options. + # - { jobs: ['run_cpu'], project: 'target', ctk: ['12.X', '13.X'], cxx: ['gcc', 'clang', 'msvc'], + # args: '--preset cub-cpp20 --build-targets "cub.cpp20.test.iterator"' } + # - { jobs: ['run_gpu'], project: 'target', ctk: ['12.X', '13.X'], cxx: ['gcc', 'clang'], gpu: 'rtxa6000', + # args: '--preset cub-cpp20 --build-targets "cub.cpp20.test.iterator" --ctest-targets "cub.cpp20.test.iterator"' } + # - { jobs: ['run_cpu'], project: 'target', ctk: ['12.X', '13.X'], cxx: ['gcc', 'clang', 'msvc'], + # args: '--preset libcudacxx --lit-precompile-tests "cuda/utility/basic_any.pass.cpp"' } + # - { jobs: ['run_gpu'], project: 'target', ctk: ['12.X', '13.X'], cxx: ['gcc', 'clang'], gpu: 'rtx2080', + # args: '--preset libcudacxx --lit-tests "cuda/utility/basic_any.pass.cpp"' } + # + # IMPORTANT: Do NOT delete or remove the `override:` key below, even when it is empty. + override: + + pull_request: + # Old CTK: Oldest/newest supported host compilers: + - {jobs: ['build'], std: 'minmax', ctk: '12.0', cxx: ['gcc12', 'clang14', 'msvc2019', 'msvc14.39']} + - {jobs: ['build'], project: ['libcudacxx', 'thrust'], std: 'minmax', ctk: '12.0', cxx: 'gcc7'} + - {jobs: ['build_nolid', 'build_lid1', 'build_lid2'], project: 'cub', std: 'minmax', ctk: '12.0', cxx: 'gcc7'} + # CTK12.0/GCC7 CUB host-launch builds are memory-heavy with benchmarks enabled; keep this shard below + # the 61 GiB linux-amd64-cpu16 runner limit. + - {jobs: ['build_lid0'], project: 'cub', std: 'minmax', ctk: '12.0', cxx: 'gcc7', environment: ['PARALLEL_LEVEL=8']} + - {jobs: ['build'], std: 'minmax', ctk: '12.X', cxx: ['gcc7', 'gcc14', 'clang14', 'clang19', 'msvc2019', 'msvc2022' ]} + - {jobs: ['build'], std: 'minmax', ctk: '13.0', cxx: ['gcc11', 'gcc15', 'clang15', 'clang20', 'msvc2019', 'msvc2022' ]} + # Old CTK: cudax has a different support matrix: + - {jobs: ['build'], project: 'cudax', ctk: '12.0', std: 'minmax', cxx: ['gcc9', 'gcc12', 'clang14', 'msvc14.39']} + - {jobs: ['build'], project: 'cudax', ctk: '12.X', std: 'minmax', cxx: ['gcc9', 'gcc14', 'clang14', 'clang19', 'msvc2022']} + - {jobs: ['build'], project: 'cudax', ctk: '13.0', std: 'minmax', cxx: ['gcc11', 'gcc15', 'clang15', 'clang20', 'msvc2022']} + # Current CTK build-only: + - {jobs: ['build'], std: 'minmax', cxx: ['gcc11', 'clang15', 'msvc2019'] } # Oldest + - {jobs: ['build'], std: 'max', cxx: ['gcc12', 'gcc13', 'gcc14'] } + - {jobs: ['build'], std: 'max', cxx: ['clang16', 'clang17', 'clang18', 'clang19', 'clang20'] } + - {jobs: ['build'], std: 'max', cxx: ['msvc2022'] } + - {jobs: ['build'], std: 'all', cxx: ['gcc', 'clang', 'msvc']} # Latest + # Current CTK build-only: cudax has a different support matrix: + - {jobs: ['build'], project: 'cudax', std: 'minmax', cxx: ['gcc11', 'clang15', 'msvc2022']} # Oldest + - {jobs: ['build'], project: 'cudax', std: 'max', cxx: ['gcc12']} + - {jobs: ['build'], project: 'cudax', std: 'max', cxx: ['clang16', 'clang17', 'clang18', 'clang19', 'clang20']} + - {jobs: ['build'], project: 'cudax', std: 'all', cxx: ['gcc', 'clang', 'msvc']} # Newest + # Current CTK testing: + - {jobs: ['test'], project: 'thrust', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtx4090'} + - {jobs: ['test'], project: ['libcudacxx', 'cudax'], std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 't4'} + + - {jobs: ['test_nolid', 'test_lid0'], project: 'cub', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtxa6000'} + - {jobs: ['test_lid1', 'test_lid2'], project: 'cub', std: 'max', cxx: ['gcc'], gpu: 'rtxa6000'} + # H100 coverage: + - {jobs: ['test_nolid', 'test_lid0'], project: 'cub', std: 'max', gpu: 'h100' } + - {jobs: ['test_gpu'], project: 'thrust', std: 'max', gpu: 'h100' } + - {jobs: ['test'], project: ['libcudacxx', 'cudax'], std: 'max', gpu: 'h100' } + # Multi-GPU coverage: + - {jobs: ['test'], project: ['libcudacxx', 'cudax'], std: 'max', gpu: 'h100_2gpu', sm: 'gpu'} + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test_lid0'], project: 'cub', std: 'max', cxx: 'gcc', gpu: 'rtxpro6000'} + # Misc: + - {jobs: ['build'], cpu: 'arm64', project: ['libcudacxx', 'cub', 'thrust', 'cudax'], std: 'max', cxx: ['gcc', 'clang']} + - {jobs: ['test_gpu'], project: 'thrust', cmake_options: '-DTHRUST_DISPATCH_TYPE=Force32bit', gpu: 'rtx4090'} + - {jobs: ['nvrtc'], project: 'libcudacxx', std: 'all', gpu: 'rtx2080', sm: 'gpu'} + - {jobs: ['verify_codegen'], project: 'libcudacxx'} + # c.parallel -- pinned to gcc13 / msvc2022 to match python + - {jobs: ['test'], project: 'cccl_c_parallel', ctk: '12.X', cxx: ['gcc13', 'msvc2022'], gpu: ['t4']} + - {jobs: ['test'], project: 'cccl_c_parallel', ctk: '13.X', cxx: ['gcc13', 'msvc2022'], gpu: ['rtx2080', 'l4', 'h100']} + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test'], project: 'cccl_c_parallel', ctk: '13.X', cxx: ['gcc13'], gpu: ['rtxpro6000']} + # c.parallel v2 (HostJIT-based) + # + # For now, this is a separate job run for Linux/CUDA13. + # Eventually v2 will replace v1 as the default and run across the + # entire matrix. Currently blocked on libnvfatbin availability on + # Windows containers, and for CUDA <12.4. + - {jobs: ['test'], project: 'cccl_c_parallel_v2', ctk: '13.X', cxx: ['gcc13', 'msvc'], gpu: 'rtx2080'} + # Python against c.parallel v2 (HostJIT-based). Single point of coverage + # for the v2 Python path; the main `python` matrix continues to test + # against v1 until v2 replaces it. + - {jobs: ['test'], project: 'python_v2', ctk: '13.X', py_version: '3.14', gpu: 'l4', cxx: 'gcc13'} + - {jobs: ['test_py_compute_minimal'], project: 'python_v2', ctk: '13.X', py_version: '3.14t', gpu: 'l4', cxx: 'gcc13'} + # c.experimental.stf-- pinned to gcc13 to match python + - {jobs: ['test'], project: 'cccl_c_stf', ctk: ['12.X', '13.X'], cxx: 'gcc13', gpu: 't4'} + - {jobs: ['test'], project: 'cccl_c_stf', ctk: '13.X', cxx: 'gcc13', gpu: ['l4', 'h100']} + # Python -- pinned to gcc13 / msvc2022 for consistency across CTK images + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: ['3.10'], gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X','13.0', '13.X'], py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', py_version: '3.14', gpu: 'h100', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: '13.X', py_version: '3.14', gpu: ['t4', 'rtxa6000', 'rtxpro6000'], cxx: 'gcc13'} + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: '13.X', py_version: '3.14', gpu: 'l4', cxx: 'gcc13'} + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14t', gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test_py_compute_minimal'], project: 'python_tsan', ctk: '13.X', py_version: '3.14t', gpu: 'l4', cxx: 'gcc13'} + # Deliberately unpinned: py_ctk_mode 'latest' skips the CTK pin so pip + # resolves the latest minor -- catching breakage a plain `pip install + # cuda-cccl[cu12]/[cu13]` would hit before the container CTK bumps. + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'latest'} + # sysctk: install the sysctk extras (system-provided CTK, no pip cuda-toolkit) + # rather than cu*. + # Full test on 3.14, minimal (numba-free) on 3.14t; all CTKs, Linux+Windows. + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14t', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + # cuda.cccl.headers: CPU-only (gpu:false), arch-insensitive + - {jobs: ['test_headers'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: ['pinned', 'latest', 'sysctk']} + # CCCL packaging: + - {jobs: ['test'], project: 'packaging', ctk: '12.0', cxx: ['gcc10', 'clang14'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '12.X', cxx: ['gcc10', 'clang14'], gpu: 't4'} + - {jobs: ['test'], project: 'packaging', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 't4'} + - {jobs: ['install'], project: 'packaging'} + # NVBench Helper testing: + - {jobs: ['test'], project: 'nvbench_helper', ctk: ['12.0', '12.X'], cxx: ['gcc10', 'clang14'], gpu: 't4'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 't4'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 't4'} + # NVHPC build + - {jobs: ['build'], cxx: 'nvhpc', ctk: 'nvhpc', std: 'max', project: ['libcudacxx', 'thrust', 'stdpar'], cpu: 'amd64'} + - {jobs: ['build_nolid'], cxx: 'nvhpc', ctk: 'nvhpc', std: 'max', project: 'cub', cpu: 'amd64'} + # clang-cuda + - {jobs: ['build'], cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 'all', sm: '75;80;90;100;120'} + - {jobs: ['build'], project: 'libcudacxx', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 23, sm: '75;80;90;100;120'} + # libc++ + # - arm64 for now as it's closest to android. + # - {jobs: ['build'], cpu: 'arm64', project: 'libcudacxx', std: 'all', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', cmake_options: '-DCCCL_USE_LIBCXX=ON', sm: '75;80;90;100;120'} + # clang-tidy + # + # The precise value of sm is not important, but it is required for cmake to identify + # clang as a CUDA compiler (see + # https://discourse.cmake.org/t/cmake-cuda-clang-fails/8657/5). + # + # Standard being exactly "min" is required. clang-tidy may emit additional diagnostics + # for later C++ versions (for example, warning that you should use designated + # initializers in C++20 or higher). + - { jobs: ['build'], project: 'tidy', std: 'min', cxx: ['clang'], cudacxx: ['clang'], ctk: 'clang-cuda', sm: '75' } + + # Used when an upstream project changes to reduce time spent smoke testing dependencies. + pull_request_lite: + # libcudacxx - Specialized, testing default SM + - {project: 'libcudacxx', jobs: ['test'], std: 'max', cxx: ['gcc', 'msvc'], gpu: 'rtx2080', sm: 'gpu'} + - {project: 'libcudacxx', jobs: ['build'], std: 'max', cxx: 'clang'} + - {project: 'libcudacxx', jobs: ['build'], std: 'max', ctk: 'nvhpc', cxx: 'nvhpc'} + - {project: 'libcudacxx', jobs: ['build'], std: 'max', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', sm: '70;80;90;100;120'} + - {project: 'libcudacxx', jobs: ['nvrtc'], std: 'max', gpu: 't4', sm: 'gpu'} + - {project: 'libcudacxx', jobs: ['verify_codegen']} + # CUB - Specialized, testing default SM + - {project: 'cub', jobs: ['test_nolid', 'test_lid0'], std: 'max', cxx: ['gcc', 'msvc'], gpu: 'rtxa6000', sm: 'gpu'} + - {project: 'cub', jobs: ['build_nolid', 'build_lid0'], std: 'max', cxx: 'clang'} + - {project: 'cub', jobs: ['build_nolid', 'build_lid0'], std: 'max', ctk: 'nvhpc', cxx: 'nvhpc'} + - {project: 'cub', jobs: ['build_nolid', 'build_lid0'], std: 'max', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', sm: '75;80;90;100;120'} + # Thrust - Keep number of sm small. Kernel coverage is in CUB. This just tests dispatch / glue in lite mode: + - {project: 'thrust', jobs: ['test'], std: 'max', cxx: ['gcc', 'msvc'], gpu: 'rtx4090', sm: 'gpu'} + - {project: 'thrust', jobs: ['build'], std: 'max', cxx: 'clang', sm: '75;120'} + - {project: 'thrust', jobs: ['build'], std: 'max', ctk: 'nvhpc', cxx: 'nvhpc', sm: '75;120'} + - {project: 'thrust', jobs: ['build'], std: 'max', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', sm: '75;120'} + # cudax + - {project: 'cudax', jobs: ['test'], std: 'max', cxx: ['gcc', 'msvc'], gpu: 'rtx2080', sm: 'gpu'} + - {project: 'cudax', jobs: ['build'], std: 'max', cxx: 'clang', sm: '75;120'} + - {project: 'cudax', jobs: ['build'], std: 'max', ctk: 'nvhpc', cxx: 'nvhpc', sm: '75;120'} + # stdpar + - {project: 'stdpar', jobs: ['build'], std: 'max', ctk: 'nvhpc', cxx: 'nvhpc'} + # Python + support + - {project: 'cccl_c_parallel', jobs: ['test'], ctk: '13.X', cxx: ['gcc13', 'msvc2022'], gpu: 'rtx2080', sm: 'gpu'} + - {project: 'cccl_c_parallel', jobs: ['test'], ctk: '13.X', cxx: 'gcc13', gpu: 'rtxpro6000', sm: 'gpu'} + - {project: 'cccl_c_stf', jobs: ['test'], ctk: '13.X', cxx: 'gcc13', gpu: 't4', sm: 'gpu'} + - {project: 'python', jobs: ['test'], ctk: '13.X', py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {project: 'python', jobs: ['test_headers'], ctk: '13.X', py_version: '3.14', cxx: ['gcc13', 'msvc2022']} + # Packaging / install + - {project: 'packaging', jobs: ['test'], ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 'rtx2080', sm: 'gpu'} + - {project: 'packaging', jobs: ['test'], args: '-min-cmake', gpu: 't4', sm: 'gpu'} + - {project: 'packaging', jobs: ['install']} + # NVBench Helper testing: + - {project: 'nvbench_helper', jobs: ['test'], ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 'rtx2080'} + # c.parallel v2 (HostJIT-based) + - {jobs: ['test'], project: 'cccl_c_parallel_v2', ctk: '13.X', cxx: ['gcc13'], gpu: 'rtx2080'} + # Python against c.parallel v2 (HostJIT-based) + - {jobs: ['test'], project: 'python_v2', ctk: '13.X', py_version: '3.14', gpu: 'l4', cxx: 'gcc13'} + + nightly: + # CTK 12.0 full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['gcc7', 'gcc8', 'gcc9', 'gcc10', 'gcc11', 'gcc12']} + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['clang14']} + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['msvc2019', 'msvc14.39']} + # CTK 12.X full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['gcc7', 'gcc8', 'gcc9', 'gcc10', 'gcc11', 'gcc12', 'gcc13', 'gcc14']} + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['clang14', 'clang15', 'clang16', 'clang17', 'clang18', 'clang19']} + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['msvc2019', 'msvc2022']} + # CTK 13.0 full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20']} + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['msvc2019', 'msvc2022']} + # CTK '13.X' full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20', 'clang21']} + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['msvc2019', 'msvc2022', 'msvc2026']} + # CTK 12.0 full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['gcc9', 'gcc10', 'gcc11', 'gcc12']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['clang14']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['msvc14.39']} + # CTK 12.X full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['gcc9', 'gcc10', 'gcc11', 'gcc12', 'gcc13', 'gcc14']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['clang14', 'clang15', 'clang16', 'clang17', 'clang18', 'clang19']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['msvc2022']} + # CTK 13.0 full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['msvc2022']} + # CTK '13.X' full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20', 'clang21']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['msvc2022', 'msvc2026']} + # CTK 12.X testing: + - {jobs: ['test'], project: 'libcudacxx', ctk: '12.X', std: 'max', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 't4'} + - {jobs: ['test'], project: 'cub', ctk: '12.X', std: 'max', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtxa6000'} + - {jobs: ['test'], project: 'thrust', ctk: '12.X', std: 'max', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtx4090'} + - {jobs: ['test'], project: 'cudax', ctk: '12.X', std: 'max', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtx2080'} + - {jobs: ['test'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '12.X', std: 'max', cxx: 'gcc14', gpu: 'h100' } + # CTK '13.X' testing: + - {jobs: ['test'], project: 'libcudacxx', ctk: '13.X', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'cub', ctk: '13.X', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtxa6000'} + - {jobs: ['test'], project: 'thrust', ctk: '13.X', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtx4090'} + - {jobs: ['test'], project: 'cudax', ctk: '13.X', std: 'max', cxx: ['gcc', 'clang', 'msvc'], gpu: 't4'} + - {jobs: ['test'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '13.X', std: 'max', gpu: 'h100' } + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test_nolid', 'test_lid0'], project: ['cub', 'thrust'], std: 'max', cxx: 'gcc', gpu: 'rtxpro6000'} + # Misc: + - {jobs: ['build'], cpu: 'arm64', project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '12.X', std: 'all', cxx: ['gcc14', 'clang19']} + - {jobs: ['build'], cpu: 'arm64', project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '13.X', std: 'all', cxx: ['gcc', 'clang']} + - {jobs: ['test_gpu'], project: 'thrust', cmake_options: '-DTHRUST_DISPATCH_TYPE=Force32bit', gpu: 'rtx4090'} + - {jobs: ['test_gpu'], project: 'thrust', cmake_options: '-DTHRUST_DISPATCH_TYPE=Force64bit', gpu: 'rtx4090'} + - {jobs: ['limited'], project: 'cub', std: 17, gpu: 'rtx2080'} + # NVRTC tests don't currently support 12.0: + - {jobs: ['nvrtc'], project: 'libcudacxx', ctk: [ '12.X', '13.0', '13.X'], cxx: 'gcc12', std: 'all', gpu: 'rtx2080', sm: 'gpu'} + - {jobs: ['verify_codegen'], project: 'libcudacxx'} + # c.parallel -- pinned to gcc13 / msvc2022 to match python + - {jobs: ['test'], project: ['cccl_c_parallel'], ctk: '12.X', cxx: ['gcc13', 'msvc2022'], gpu: ['t4']} + - {jobs: ['test'], project: ['cccl_c_parallel'], ctk: '13.X', cxx: ['gcc13', 'msvc2022'], gpu: ['rtx2080', 'l4', 'h100']} + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test'], project: 'cccl_c_parallel', ctk: '13.X', cxx: ['gcc13'], gpu: ['rtxpro6000']} + # c.experimental.stf -- pinned to gcc13 to match python + - {jobs: ['test'], project: ['cccl_c_stf'], ctk: '12.X', cxx: 'gcc13', gpu: ['rtx2080']} + - {jobs: ['test'], project: ['cccl_c_stf'], ctk: '13.X', cxx: 'gcc13', gpu: ['t4', 'l4', 'h100']} + # Python -- pinned to gcc13 / msvc2022 on Linux for consistency across CTK images + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.11', '3.12', '3.13', '3.14'], gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', gpu: 'h100', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', gpu: ['t4', 'rtxa6000'], cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: '13.X', py_version: '3.14', gpu: 'rtxpro6000', cxx: 'gcc13'} + # Python free-threaded (3.14t) minimal lanes -- mirrors the pull_request rows + # so FT regressions (e.g. from dependency bumps) surface between PRs. + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14t', gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test_py_compute_minimal'], project: 'python_v2', ctk: '13.X', py_version: '3.14t', gpu: 'l4', cxx: 'gcc13'} + - {jobs: ['test_py_compute_minimal'], project: 'python_tsan', ctk: '13.X', py_version: '3.14t', gpu: 'l4', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14t', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + # cuda.cccl.headers (CPU-only): all CTK x source x OS, py endpoints 3.10 + 3.14 + - {jobs: ['test_headers'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.14'], cxx: ['gcc13', 'msvc2022'], py_ctk_mode: ['pinned', 'latest', 'sysctk']} + # CCCL packaging: + - {jobs: ['test'], project: 'packaging', ctk: '12.0', cxx: ['gcc10', 'clang14'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '12.X', cxx: ['gcc10', 'clang14'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'packaging', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 'rtx2080'} + - {jobs: ['install'], project: 'packaging'} + # NVBench Helper testing: + - {jobs: ['test'], project: 'nvbench_helper', ctk: '12.0', cxx: ['gcc10', 'clang14'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '12.X', cxx: ['gcc10', 'clang14'], gpu: 't4'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 't4'} + # NVHPC build + - {jobs: ['build'], cxx: 'nvhpc-prev', ctk: 'nvhpc-prev', std: 'all', project: ['libcudacxx', 'cub', 'thrust', 'cudax', 'stdpar'], cpu: ['amd64', 'arm64']} + - {jobs: ['build'], cxx: 'nvhpc', ctk: 'nvhpc', std: 'all', project: ['libcudacxx', 'cub', 'thrust', 'cudax', 'stdpar'], cpu: ['amd64', 'arm64']} + # clang-cuda + - {jobs: ['build'], cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 'all', sm: '75;80;90;100;120'} + - {jobs: ['build'], cudacxx: 'clang', ctk: 'clang_preview-cuda', cxx: 'clang_preview-cuda', std: 'all', sm: '75;80;90;100;120'} + - {jobs: ['build'], project: 'libcudacxx', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 23, sm: '75;80;90;100;120'} + - {jobs: ['build'], project: 'libcudacxx', cudacxx: 'clang', ctk: 'clang_preview-cuda', cxx: 'clang_preview-cuda', std: 23, sm: '75;80;90;100;120'} + # clang-tidy + - { jobs: ['build'], project: 'tidy', std: 'min', cxx: ['clang'], cudacxx: ['clang'], ctk: 'clang-cuda', sm: '75' } + # arch-specific and family-specific arch builds + - {jobs: ['build'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], std: 'all', sm: '90a;100a;103a;110a;120a;121a'} + - {jobs: ['build'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], std: 'all', sm: '100f;103f;110f;120f;121f'} + + weekly: + # CTK 12.0 full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['gcc7', 'gcc8', 'gcc9', 'gcc10', 'gcc11', 'gcc12']} + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['clang14']} + - {jobs: ['build'], std: 'all', ctk: '12.0', cxx: ['msvc2019', 'msvc14.39']} + # CTK 12.X full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['gcc7', 'gcc8', 'gcc9', 'gcc10', 'gcc11', 'gcc12', 'gcc13', 'gcc14']} + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['clang14', 'clang15', 'clang16', 'clang17', 'clang18', 'clang19']} + - {jobs: ['build'], std: 'all', ctk: '12.X', cxx: ['msvc2019', 'msvc2022']} + # CTK 13.0 full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20']} + - {jobs: ['build'], std: 'all', ctk: '13.0', cxx: ['msvc2019', 'msvc2022']} + # CTK '13.X' full matrix build: default projects + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20', 'clang21']} + - {jobs: ['build'], std: 'all', ctk: '13.X', cxx: ['msvc2019', 'msvc2022', 'msvc2026']} + # CTK 12.0 full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['gcc9', 'gcc10', 'gcc11', 'gcc12']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['clang14']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.0', cxx: ['msvc14.39']} + # CTK 12.X full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['gcc9', 'gcc10', 'gcc11', 'gcc12', 'gcc13', 'gcc14']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['clang14', 'clang15', 'clang16', 'clang17', 'clang18', 'clang19']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '12.X', cxx: ['msvc2022']} + # CTK 13.0 full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.0', cxx: ['msvc2022']} + # CTK '13.X' full matrix build: cudax + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['gcc11', 'gcc12', 'gcc13', 'gcc14', 'gcc15']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19', 'clang20', 'clang21']} + - {jobs: ['build'], project: 'cudax', std: 'all', ctk: '13.X', cxx: ['msvc2022', 'msvc2026']} + # CTK 12.X testing: + - {jobs: ['test'], project: 'libcudacxx', ctk: '12.X', std: 'minmax', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'cub', ctk: '12.X', std: 'minmax', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtxa6000'} + - {jobs: ['test'], project: 'thrust', ctk: '12.X', std: 'minmax', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 'rtx4090'} + - {jobs: ['test'], project: 'cudax', ctk: '12.X', std: 'minmax', cxx: ['gcc14', 'clang19', 'msvc2022'], gpu: 't4'} + - {jobs: ['test'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '12.X', std: 'minmax', cxx: 'gcc14', gpu: 'h100' } + # CTK '13.X' testing: + - {jobs: ['test'], project: 'libcudacxx', ctk: '13.X', std: 'minmax', cxx: ['gcc', 'clang', 'msvc'], gpu: 't4'} + - {jobs: ['test'], project: 'cub', ctk: '13.X', std: 'minmax', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtxa6000'} + - {jobs: ['test'], project: 'thrust', ctk: '13.X', std: 'minmax', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtx4090'} + - {jobs: ['test'], project: 'cudax', ctk: '13.X', std: 'minmax', cxx: ['gcc', 'clang', 'msvc'], gpu: 'rtx2080'} + - {jobs: ['test'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '13.X', std: 'minmax', gpu: 'h100' } + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test'], project: ['cub', 'thrust'], std: 'max', cxx: 'gcc', gpu: 'rtxpro6000'} + # Misc: + - {jobs: ['build'], cpu: 'arm64', project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '12.X', std: 'all', cxx: ['gcc14', 'clang19']} + - {jobs: ['build'], cpu: 'arm64', project: ['libcudacxx', 'cub', 'thrust', 'cudax'], ctk: '13.X', std: 'all', cxx: ['gcc', 'clang']} + - {jobs: ['test_gpu'], project: 'thrust', cmake_options: '-DTHRUST_DISPATCH_TYPE=Force32bit', gpu: 'rtx4090'} + - {jobs: ['test_gpu'], project: 'thrust', cmake_options: '-DTHRUST_DISPATCH_TYPE=Force64bit', gpu: 'rtx4090'} + - {jobs: ['limited'], project: 'cub', std: 17, gpu: 'rtx2080'} + # sm: all-cccl: + - {jobs: ['build'], project: ['thrust', 'libcudacxx', 'cudax'], std: 'max', sm: 'all-cccl' } + - {jobs: ['build_nolid', 'build_lid0'], project: ['cub'], std: 'max', sm: 'all-cccl'} + # NVRTC tests don't currently support 12.0: + - {jobs: ['nvrtc'], project: 'libcudacxx', ctk: [ '12.X', '13.0', '13.X'], cxx: 'gcc12', std: 'all', gpu: 'rtx2080', sm: 'gpu'} + - {jobs: ['verify_codegen'], project: 'libcudacxx'} + # c.parallel -- pinned to gcc13 / msvc2022 to match python + - {jobs: ['test'], project: ['cccl_c_parallel'], ctk: '12.X', cxx: ['gcc13', 'msvc2022'], gpu: ['t4']} + - {jobs: ['test'], project: ['cccl_c_parallel'], ctk: '13.X', cxx: ['gcc13', 'msvc2022'], gpu: ['rtx2080', 'l4', 'h100']} + # RTX PRO 6000 coverage (limited due to small number of runners): + - {jobs: ['test'], project: 'cccl_c_parallel', ctk: '13.X', cxx: ['gcc13'], gpu: ['rtxpro6000']} + # c.experimental.stf -- pinned to gcc13 to match python + - {jobs: ['test'], project: ['cccl_c_stf'], ctk: '12.X', cxx: 'gcc13', gpu: ['rtx2080']} + - {jobs: ['test'], project: ['cccl_c_stf'], ctk: '13.X', cxx: 'gcc13', gpu: ['t4', 'l4', 'h100']} + # Python -- pinned to gcc13 / msvc2022 for consistency across CTK images + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.11', '3.12', '3.13', '3.14'], gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', gpu: 'h100', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.14'], gpu: 't4', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14', gpu: 'rtxa6000', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: '13.X', py_version: '3.14', gpu: 'rtxpro6000', cxx: 'gcc13'} + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + - {jobs: ['test_py_compute_minimal'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: '3.14t', gpu: 'l4', cxx: ['gcc13', 'msvc2022'], py_ctk_mode: 'sysctk'} + # cuda.cccl.headers (CPU-only): all CTK x source x OS, py endpoints 3.10 + 3.14 + - {jobs: ['test_headers'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.14'], cxx: ['gcc13', 'msvc2022'], py_ctk_mode: ['pinned', 'latest', 'sysctk']} + # CCCL packaging: + - {jobs: ['test'], project: 'packaging', ctk: '12.0', cxx: ['gcc10', 'clang14'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '12.X', cxx: ['gcc10', 'clang14'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'packaging', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 't4', args: '-min-cmake'} + - {jobs: ['test'], project: 'packaging', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 'rtx2080'} + - {jobs: ['install'], project: 'packaging'} + # NVBench Helper: + - {jobs: ['test'], project: 'nvbench_helper', ctk: '12.0', cxx: ['gcc10', 'clang14'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '12.X', cxx: ['gcc10', 'clang14'], gpu: 't4'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.0', cxx: ['gcc15', 'clang20'], gpu: 'rtx2080'} + - {jobs: ['test'], project: 'nvbench_helper', ctk: '13.X', cxx: ['gcc', 'clang'], gpu: 't4'} + # NVHPC build + - {jobs: ['build'], cxx: 'nvhpc-prev', ctk: 'nvhpc-prev', std: 'all', project: ['libcudacxx', 'cub', 'thrust', 'cudax', 'stdpar'], cpu: ['amd64', 'arm64']} + - {jobs: ['build'], cxx: 'nvhpc', ctk: 'nvhpc', std: 'all', project: ['libcudacxx', 'cub', 'thrust', 'cudax', 'stdpar'], cpu: ['amd64', 'arm64']} + # clang-cuda + - {jobs: ['build'], cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 'all', sm: '75;80;90;100;120'} + - {jobs: ['build'], cudacxx: 'clang', ctk: 'clang_preview-cuda', cxx: 'clang_preview-cuda', std: 'all', sm: '75;80;90;100;120'} + - {jobs: ['build'], project: 'libcudacxx', cudacxx: 'clang', ctk: 'clang-cuda', cxx: 'clang-cuda', std: 23, sm: '75;80;90;100;120'} + - {jobs: ['build'], project: 'libcudacxx', cudacxx: 'clang', ctk: 'clang_preview-cuda', cxx: 'clang_preview-cuda', std: 23, sm: '75;80;90;100;120'} + # compute-sanitizer + - {jobs: ['compute_sanitizer'], project: 'cub', std: 'max', gpu: 'rtxa6000', sm: 'gpu', cmake_options: '-DCMAKE_CUDA_FLAGS=-lineinfo'} + # clang-tidy + - { jobs: ['build'], project: 'tidy', std: 'min', cxx: ['clang'], cudacxx: ['clang'], ctk: 'clang-cuda', sm: '75' } + # arch-specific and family-specific arch builds + - {jobs: ['build'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], std: 'all', sm: '90a;100a;103a;110a;120a;121a'} + - {jobs: ['build'], project: ['libcudacxx', 'cub', 'thrust', 'cudax'], std: 'all', sm: '100f;103f;110f;120f;121f'} + + python-wheels: + - {jobs: ['test'], project: 'python', ctk: ['12.0', '12.X', '13.0', '13.X'], py_version: ['3.10', '3.11', '3.12', '3.13', '3.14'], gpu: 'l4', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', ctk: ['12.X', '13.X'], py_version: '3.14', gpu: 'h100', cxx: ['gcc13', 'msvc2022']} + - {jobs: ['test'], project: 'python', cpu: 'arm64', ctk: ['12.X', '13.X'], py_version: ['3.10', '3.11', '3.12', '3.13', '3.14'], gpu: 'l4', cxx: 'gcc13'} + + + # This is just used to ensure that we generate devcontainers for all images we build. + # These do not map to any actual jobs. + devcontainers: + - {jobs: ['dc'], ctk: ['12.0', '12.X' ], cxx: ['clang14']} + - {jobs: ['dc'], ctk: ['12.0', '12.X' ], cxx: ['gcc7', 'gcc8', 'gcc9', 'gcc10']} + - {jobs: ['dc'], ctk: ['12.0', '12.X', '13.0', '13.X'], cxx: ['gcc11', 'gcc12']} + - {jobs: ['dc'], ctk: [ '12.X', '13.0', '13.X'], cxx: ['gcc13', 'gcc14']} + - {jobs: ['dc'], ctk: [ '13.0', '13.X'], cxx: ['gcc15']} + - {jobs: ['dc'], ctk: [ '12.X', '13.0', '13.X'], cxx: ['clang15', 'clang16', 'clang17', 'clang18', 'clang19']} + - {jobs: ['dc_ext'], ctk: [ '12.X', '13.0', '13.X'], cxx: ['gcc14', 'clang20']} + # Clang21+CTK12.9 is currently only used for cuda-clang testing. nvcc 12.9 doesn't support clang21. + - {jobs: ['dc_ext'], ctk: [ '12.X', '13.X'], cxx: ['clang21']} + - {jobs: ['dc_ext'], ctk: [ '13.X'], cxx: ['gcc15']} + # Clang22+CTK12.9 is currently only used for clang-cuda testing. nvcc 12.9 doesn't support clang22. + - {jobs: ['dc_ext'], ctk: [ '12.X' ], cxx: ['clang_preview22']} + # 12.0 python image, pinned at gcc13 for simplicity. CTK 12.0 doesn't really play nice with gcc13, but + # that doesn't matter for running python tests. + - {jobs: ['dc'], ctk: ['12.0'], cxx: 'gcc13'} + # NVHPC + - {jobs: ['dc'], cxx: 'nvhpc-prev', ctk: 'nvhpc-prev'} + - {jobs: ['dc'], cxx: 'nvhpc', ctk: 'nvhpc'} + + # Any generated jobs that match the entries in `exclude` will be removed from the final matrix for all workflows. + exclude: + # GPU runners are not available on Windows. + - {jobs: ['test', 'test_gpu', 'test_nolid', 'test_lid0', 'test_lid1', 'test_lid2'], cxx: ['msvc2019', 'msvc14.39', 'msvc2022', 'msvc2026']} + # cudax doesn't support C++17 on msvc: + - {project: 'cudax', std: 17, cxx: ['msvc2019', 'msvc14.39', 'msvc2022', 'msvc2026']} + + +############################################################################################# + + +# The version of the devcontainer images to use from https://hub.docker.com/r/rapidsai/devcontainers +devcontainer_version: '26.10' + +# Compiler versions used for the cuda99.X internal builds: +cuda99_gcc_version: 15 +cuda99_clang_version: 21 + +# All supported C++ standards: +all_stds: [17, 20] + +# Aliases: +# - 12.X: Newest CTK 12.X version. +# - 13.X: Newest CTK 13.X version. +# - nvhpc: CTK shipped in newest NVHPC +# - nvhpc-prev: CTK shipped in previous NVHPC +# - pybuild: Selects image to use for python wheel builds' outer docker instance +ctk_versions: + 12.0: { stds: [17, 20] } + 12.9: { stds: [17, 20], alias: ['12.X', 'pybuild', 'clang-cuda', 'clang_preview-cuda'] } + 13.0: { stds: [17, 20] } + 13.1: { stds: [17, 20], alias: ['nvhpc-prev']} + 13.2: { stds: [17, 20], alias: ['nvhpc']} + 13.3: { stds: [17, 20], alias: ['13.X'] } + +device_compilers: + nvcc: # Version / stds are taken from CTK + name: 'nvcc' + exe: 'nvcc' + clang: # Requires cxx=clang. Version / stds are taken from cxx compiler. + name: "ClangCUDA" + exe: 'clang++' + +host_compilers: + gcc: + name: 'GCC' + container_tag: 'gcc' + exe: 'g++' + versions: + 7: { stds: [17, ] } + 8: { stds: [17, ] } + 9: { stds: [17, ] } + 10: { stds: [17, 20] } + 11: { stds: [17, 20] } + 12: { stds: [17, 20] } + 13: { stds: [17, 20] } + 14: { stds: [17, 20] } + 15: { stds: [17, 20] } + clang: + name: 'Clang' + container_tag: 'llvm' + exe: 'clang++' + versions: + 14: { stds: [17, 20] } + 15: { stds: [17, 20] } + 16: { stds: [17, 20] } + 17: { stds: [17, 20] } + 18: { stds: [17, 20] } + 19: { stds: [17, 20] } + 20: { stds: [17, 20] } + 21: { stds: [17, 20], alias: 'cuda' } + clang_preview: + name: 'Clang' + container_tag: 'llvm' + exe: 'clang++' + versions: + 22: { stds: [17, 20], alias: 'cuda' } + msvc: + name: 'MSVC' + container_tag: 'cl' + exe: cl + versions: + '14.29': { stds: [17, ], alias: '2019' } + '14.39': { stds: [17, 20] } # CTK 12.0 doesn't recognize >14.39 as MSVC 2022. + '14.44': { stds: [17, 20], alias: '2022' } + '14.50': { stds: [17, 20], alias: '2026' } + + nvhpc: + name: 'NVHPC' + container_tag: 'nvhpc' + exe: nvc++ + versions: + # !! Update the ctk_versions 'nvhpc*' aliases when updating NVHPC versions: + 26.3: { stds: [17, 20], alias: 'prev' } + 26.5: { stds: [17, 20] } + +# Jobs support the following properties: +# +# - name: The human-readable name of the job. Default is the capitalized job key. +# - needs: +# - A list of jobs that must be completed before this job can run. Default is an empty list. +# - These jobs are automatically added if needed: +# - Eg. "jobs: ['test']" in the workflow def will also create the required 'build' jobs. +# - gpu: Whether the job requires a GPU runner. Default is false. +# - cuda_ext: Whether the job requires a devcontainer with extra CUDA libraries. Default is false. +# - invoke: +# - Map the job type to the script invocation spec: +# - prefix: The script invocation prefix. Default is the job name. +# - args: Additional arguments to pass to the script. Default is no args. +# - The script is invoked either: +# linux: `ci/windows/_.ps1 ` +# windows: `ci/_.sh ` +# - force_producer_ctk: +# - If set, force the auto-generated producers for this job to use a specific CTK version. +# - By default, the autogenerated job's CTK version is determined by the consumer's `ctk` tag. +# - This is useful for testing the cross-testing major version compat. +# - E.g. "force_producer_ctk: '12.0'" on a test step will force the generated build step to use CTK 12.0. + +jobs: + # General: + build: { gpu: false } + test: { gpu: true, needs: 'build' } + install: { gpu: false } + + test_nobuild: { gpu: true, name: 'Test', invoke: { prefix: 'test' } } + + compute_sanitizer: { gpu: true, name: 'ComputeSanitizer', needs: 'build', invoke: { prefix: 'test', args: '-compute-sanitizer' } } + + # libcudacxx: + nvrtc: { gpu: true, name: 'NVRTC' } + verify_codegen: { gpu: false, name: 'VerifyCodegen' } + + # CUB: + build_nolid: { name: 'BuildNoLaunch', gpu: false, invoke: { prefix: 'build', args: '-no-lid'} } + build_lid0: { name: 'BuildHostLaunch', gpu: false, invoke: { prefix: 'build', args: '-lid0'} } + build_lid1: { name: 'BuildDeviceLaunch', gpu: false, invoke: { prefix: 'build', args: '-lid1'} } + build_lid2: { name: 'BuildGraphCapture', gpu: false, invoke: { prefix: 'build', args: '-lid2'} } + # NoLid -> The string `lid_X` doesn't appear in the test name. Mostly warp/block tests, old device tests, and examples. + test_nolid: { name: 'TestNoLaunch', gpu: true, needs: 'build_nolid', invoke: { prefix: 'test', args: '-no-lid --test-par 8'} } + # CUB uses `lid` to indicate launch strategies: whether CUB algorithms are: + # - launched from the host (lid0): + test_lid0: { name: 'HostLaunch', gpu: true, needs: 'build_lid0', invoke: { prefix: 'test', args: '-lid0 --test-par 8'} } + # - launched from the device (lid1): + test_lid1: { name: 'DeviceLaunch', gpu: true, needs: 'build_lid1', invoke: { prefix: 'test', args: '-lid1 --test-par 8'} } + # - captured in a CUDA graph for deferred launch (lid2): + test_lid2: { name: 'GraphCapture', gpu: true, needs: 'build_lid2', invoke: { prefix: 'test', args: '-lid2 --test-par 8'} } + # Limited build reduces the number of runtime test cases, available device memory, etc, and may be used + # to reduce test runtime in limited environments. + limited: { name: "SmallGMem", gpu: true, needs: 'build', invoke: { prefix: 'test', args: '-limited'} } + # Compute sanitizer jobs: + compute_mem_nolid: { name: 'CSMem-TestGPU', gpu: true, needs: 'build_nolid', invoke: { prefix: 'test', args: '-compute-sanitizer-memcheck -no-lid'} } + compute_mem_lid0: { name: 'CSMem-HostLaunch', gpu: true, needs: 'build_lid0', invoke: { prefix: 'test', args: '-compute-sanitizer-memcheck -lid0'} } + compute_race_nolid: { name: 'CSRace-TestGPU', gpu: true, needs: 'build_nolid', invoke: { prefix: 'test', args: '-compute-sanitizer-racecheck -no-lid'} } + compute_race_lid0: { name: 'CSRace-HostLaunch', gpu: true, needs: 'build_lid0', invoke: { prefix: 'test', args: '-compute-sanitizer-racecheck -lid0'} } + compute_init_nolid: { name: 'CSInit-TestGPU', gpu: true, needs: 'build_nolid', invoke: { prefix: 'test', args: '-compute-sanitizer-initcheck -no-lid'} } + compute_init_lid0: { name: 'CSInit-HostLaunch', gpu: true, needs: 'build_lid0', invoke: { prefix: 'test', args: '-compute-sanitizer-initcheck -lid0'} } + compute_sync_nolid: { name: 'CSSync-TestGPU', gpu: true, needs: 'build_nolid', invoke: { prefix: 'test', args: '-compute-sanitizer-synccheck -no-lid'} } + compute_sync_lid0: { name: 'CSSync-HostLaunch', gpu: true, needs: 'build_lid0', invoke: { prefix: 'test', args: '-compute-sanitizer-synccheck -lid0'} } + + # Thrust: + test_cpu: { name: 'TestCPU', gpu: false, needs: 'build', invoke: { prefix: 'test', args: '-cpu-only'} } + test_gpu: { name: 'TestGPU', gpu: true, needs: 'build', invoke: { prefix: 'test', args: '-gpu-only'} } + + # Python: + build_py_wheel: { name: "Build cuda.cccl", gpu: false, invoke: { prefix: 'build_cuda_cccl'} } + test_headers: { name: "Test cuda.cccl.headers", gpu: false, needs: 'build_py_wheel', force_producer_ctk: "pybuild", invoke: { prefix: 'test_cuda_cccl_headers'} } + test_py_par: { name: "Test cuda.compute", gpu: true, needs: 'build_py_wheel', force_producer_ctk: "pybuild", invoke: { prefix: 'test_cuda_compute'} } + test_py_compute_minimal: { name: "Test cuda.compute minimal", gpu: true, needs: 'build_py_wheel', force_producer_ctk: "pybuild", invoke: { prefix: 'test_cuda_compute_minimal'} } + test_py_examples: { name: "Test cuda.cccl.examples", gpu: true, needs: 'build_py_wheel', force_producer_ctk: "pybuild", invoke: { prefix: 'test_cuda_cccl_examples'} } + + # Run jobs for 'target' project (ci/util/build_and_test_targets.sh): + run_cpu: { gpu: false } + run_gpu: { gpu: true } + + # Only used for generating devcontainers. No scripts actually exist for these: + dc: { gpu: false } + dc_ext: { gpu: false, cuda_ext: true } + +# Projects have the following properties: +# +# Keys are project subdirectories names. These will also be used in script names. +# +# - stds: A list of C++ standards to test. Required. +# - name: The human-readable name of the project. Default is the project key. +# - job_map: Map general jobs to arrays of project-specific jobs. +# Useful for things like splitting cpu/gpu testing for a project. +# E.g. "job_map: { test: ['test_cpu', 'test_gpu'] }" replaces +# the "test" job with distinct "test_cpu" and "test_gpu" jobs. +projects: + packaging: + name: 'CCCL Packaging' + stds: [17, 20] + job_map: + build: [] + test: ['test_nobuild'] + libcudacxx: + name: 'libcu++' + stds: [17, 20] + cub: + name: 'CUB' + stds: [17, 20] + job_map: + build: ['build_nolid', 'build_lid0', 'build_lid1', 'build_lid2'] + test: ['test_nolid', 'test_lid0', 'test_lid1', 'test_lid2'] + compute_sanitizer: + - compute_mem_nolid + - compute_mem_lid0 + - compute_race_nolid + - compute_race_lid0 + - compute_init_nolid + - compute_init_lid0 + - compute_sync_nolid + - compute_sync_lid0 + thrust: + name: 'Thrust' + stds: [17, 20] + job_map: { test: ['test_cpu', 'test_gpu'] } + cudax: + stds: [17, 20] + stdpar: + name: 'NVHPC stdpar' + stds: [17, 20] + python: + name: "Python" + job_map: + build: ['build_py_wheel'] + test: ['test_py_par', 'test_py_examples'] + python_v2: + name: "Python (cuda.compute on v2/HostJIT)" + # Only cuda.compute differs between v1 and v2; cuda.cccl.headers does not + # need separate coverage. Run the examples because they exercise + # cuda.compute against the v2 backend. + job_map: + build: ['build_py_wheel'] + test: ['test_py_par', 'test_py_examples'] + python_tsan: + name: "Python (cuda.compute free-threaded ThreadSanitizer)" + # Runs per-PR (and nightly). The producer build_py_wheel runs + # build_cuda_cccl_python_tsan.sh (c.parallel v1 host code built with + # -fsanitize=thread); test_py_compute_minimal runs + # test_cuda_compute_minimal_python_tsan.sh (the FT stress + sweep under the + # TSan runtime). Deliberately absent from the `python-wheels` publish + # workflow, so these instrumented wheels never reach PyPI. + job_map: + build: ['build_py_wheel'] + test: ['test_py_compute_minimal'] + cccl_c_parallel: + name: 'CCCL C Parallel' + stds: [20] + cccl_c_parallel_v2: + name: 'CCCL C Parallel v2 (HostJIT)' + stds: [20] + # test_cccl_c_parallel_v2.sh builds inline (no separate build script), + # so suppress the default test→build dependency. test_nobuild invokes + # test_.sh directly without a producer build job. + job_map: + build: [] + test: ['test_nobuild'] + cccl_c_stf: + name: 'CCCL C CUDASTF' + stds: [20] + nvbench_helper: + name: 'NVBench Helper' + stds: [17] # Only builds on oldest arch for max compat. + job_map: + build: [] + test: ['test_nobuild'] + + # Run specific build_and_test_targets.sh invocations across the CI matrix. + # Use the override workflow and supply arguments via the `args` tag. + # Example: + # override: + # - { jobs: ['run'], project: 'target', ctk: ['12.X', '13.X'], cxx: 'gcc', gpu: 'rtx2080', + # args: '--preset cub-cpp20 --build-targets "cub.cpp20.test.iterator" --ctest-targets "cub.cpp20.test.iterator"' } + target: + name: 'Target' + stds: [17, 20] + bisect: + name: 'Bisect' + stds: [17, 20] + tidy: + name: 'clang-tidy' + stds: [17] + +# name -> Display name for generated job labels. +# runner -> GPU/driver/count segment of the GHA runner label. +# testing -> Runner with GPU is in a nv-gh-runners testing pool. +gpus: + t4: { name: 'T4', sm: 75, runner: 't4-latest-1' } # 16 GB, 10 runners + rtx2080: { name: 'RTX2080', sm: 75, runner: 'rtx2080-latest-1' } # 8 GB, 12 runners + rtxa6000: { name: 'RTXA6000', sm: 86, runner: 'rtxa6000-latest-1' } # 48 GB, 12 runners + l4: { name: 'L4', sm: 89, runner: 'l4-latest-1' } # 24 GB, 48 runners + rtx4090: { name: 'RTX4090', sm: 89, runner: 'rtx4090-latest-1' } # 24 GB, 10 runners + h100: { name: 'H100', sm: 90, runner: 'h100-latest-1' } # 80 GB, 16 runners + h100_2gpu: { name: 'H100 2-GPU', sm: 90, runner: 'h100-latest-2' } # 2 x 80 GB + # Very small number of runners on loan from cuda-python while we wait for our order to arrive. + # Limit jobs on these: + rtxpro6000: { name: 'RTXPRO6000', sm: 120, runner: 'rtxpro6000-latest-1' } + +# Tags are used to define a `matrix job` in the workflow section. +# +# Tags have the following options: +# - required: Whether the tag is required. Default is false. +# - default: The default value for the tag. Default is null. +tags: + # An array of jobs (e.g. 'build', 'test', 'nvrtc', 'infra', 'verify_codegen', ...) + # See the `jobs` map. + jobs: { required: true } + # CUDA ToolKit version + # See the `ctks` map. + ctk: { default: '13.X' } + # CPU architecture + cpu: { default: 'amd64' } + # GPU model + gpu: { default: 'rtx2080' } + # Host compiler {name, version, exe} + # See the `host_compilers` map. + cxx: { default: 'gcc' } + # Device compiler. + # See the `device_compilers` map. + cudacxx: { default: 'nvcc' } + # Project name (e.g. libcudacxx, cub, thrust, cccl) + # See the `projects` map. + project: { default: ['libcudacxx', 'cub', 'thrust'] } + # Python version for Python builds/tests + py_version: { required: false } + # Python CTK-source mode for test lanes (Python-only): + # 'pinned' (default) -- pin cuda-toolkit to the container CTK minor + # 'latest' -- unpinned; pip resolves the newest minor + # 'sysctk' -- use the system toolkit; install the sysctk extras + # If set, passed to the script with `-ctk-mode `. Exploded if an array, + # e.g. `['pinned','sysctk']`. + py_ctk_mode: { required: false } + # C++ standard + # If set to 'all', all stds supported by the ctk/compilers/project are used. + # If set to 'min', 'max', or 'minmax', the minimum, maximum, or both stds are used. + # If set, will be passed to script with `-std `. + std: { required: false } + # GPU architecture + # - If set, passed to script with `-arch `. + # - Format is the same as `CMAKE_CUDA_ARCHITECTURES`: + # - PTX only: 70-virtual + # - SASS only: 70-real + # - Both: 70 + # - Can pass multiple architectures via "60;70-real;80-virtual" + # - Defaults to use the settings in the CMakePresets.json file. + # - Will be exploded if an array, e.g. `sm: ['60;70;80;90', '90a']` creates two jobs. + # - Set to 'gpu' to only target the GPU in the `gpu` tag. + sm: { required: false } + # Additional CMake options to pass to the build. + # If set, passed to script with `-cmake_options ""`. + cmake_options: { required: false } + # Environment variables to add + environment: { required: false, default: [] } + # Additional arguments appended to the generated command. + # Typically used with the `target` project to forward options to + # ci/util/build_and_test_targets.sh, but works with all CI jobs. + args: { required: false, default: "" } diff --git a/cccl_upstream/ci/matx/build_matx.sh b/cccl_upstream/ci/matx/build_matx.sh new file mode 100755 index 00000000..1bf687aa --- /dev/null +++ b/cccl_upstream/ci/matx/build_matx.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly matx_repo=https://github.com/NVIDIA/MatX.git +readonly matx_branch=main + +# Ensure the script is being executed in the root cccl directory: +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../.."; + +log_vars() { + for var in "$@"; do + echo "${var}=${!var}" + done +} + +# Take two version strings and return the greater of the two: +version_max() { + local v1="${1}" + local v2="${2}" + if ci/util/version_compare.sh "$v1" ge "$v2"; then + echo "$v1" + else + echo "$v2" + fi +} + +# Get the current CCCL info: +readonly cccl_repo="${PWD}" + +# Define CCCL_TAG to override the default CCCL SHA. Otherwise the current HEAD of the local checkout is used. +echo "CCCL_TAG (override): ${CCCL_TAG-}"; +if test -n "${CCCL_TAG-}"; then + # If CCCL_TAG is defined, fetch it to the local checkout + git -C "${cccl_repo}" fetch origin "${CCCL_TAG}"; + cccl_sha="$(git -C "${cccl_repo}" rev-parse FETCH_HEAD)"; +else + cccl_sha="$(git -C "${cccl_repo}" rev-parse HEAD)"; +fi + +cccl_repo_version="$(git -C "${cccl_repo}" describe "${cccl_sha}"| grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')" +readonly cccl_repo_version + +# Define CCCL_VERSION to override the version used by rapids-cmake to patch CCCL. +echo "CCCL_VERSION (override): ${CCCL_VERSION-}"; +if test -n "${CCCL_VERSION-}"; then + readonly cccl_rapids_cmake_version="${CCCL_VERSION}" +else + cccl_rapids_cmake_version="${cccl_repo_version}" + # shellcheck disable=SC2034 + readonly cccl_rapids_cmake_version +fi + +# If the current version is less than 2.8.0, use 2.8.0 for the rapids-cmake version. +# This is to allow rapids-cmake to correctly patch the CCCL install rules on current `main`. +cccl_version=$(version_max "${cccl_repo_version}" "2.8.0") +readonly cccl_version + +readonly workdir="${cccl_repo}/build/${CCCL_BUILD_INFIX:-}/matx" +readonly version_file="${workdir}/MatX/cmake/versions.json" +readonly version_override_file="${workdir}/versions-override.json" + +log_vars \ + matx_repo matx_branch \ + cccl_repo cccl_sha cccl_repo_version cccl_rapids_cmake_version \ + workdir \ + version_file version_override_file + +mkdir -p "${workdir}" +cd "${workdir}" + +# Python deps: +pip install numpy + +# Clone MatX +rm -rf MatX +git clone "${matx_repo}" -b "${matx_branch}" + +cd MatX +echo "MatX HEAD:" +git log -1 --format=short +cd .. + +# Write out version override file +jq -r ".packages.CCCL *= + { + \"git_url\": \"${cccl_repo}\", + \"git_tag\": \"${cccl_sha}\", + \"version\": \"${cccl_version}\", + \"always_download\": true + }" \ + "${version_file}" > "${version_override_file}" + +echo "Overriding MatX versions.json file:" +cat "$version_override_file" + +# Configure and build +rm -rf build + +SCCACHE_NO_DIST_COMPILE=1 cmake \ + -B build -S MatX -G Ninja \ + "-DCMAKE_CUDA_ARCHITECTURES=75;120" \ + "-DRAPIDS_CMAKE_CPM_OVERRIDE_VERSION_FILE=${version_override_file}" \ + -DMATX_BUILD_TESTS=ON \ + -DMATX_BUILD_EXAMPLES=ON \ + -DMATX_BUILD_BENCHMARKS=ON \ + -DMATX_EN_CUTENSOR=ON + +# Disabled because `cmake --build -j ""` is invalid, but so is +# `cmake --build -j8`. CMake expects a space between `-j` and +# the numeric argument, or no argument at all. +# shellcheck disable=SC2086 +time cmake --build build -j ${PARALLEL_LEVEL:-} diff --git a/cccl_upstream/ci/ninja_summary.py b/cccl_upstream/ci/ninja_summary.py new file mode 100755 index 00000000..e1316f8d --- /dev/null +++ b/cccl_upstream/ci/ninja_summary.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +r"""Summarize the last ninja build, invoked with ninja's -C syntax. + +This script is designed to be automatically run after each ninja build in +order to summarize the build's performance. Making build performance information +more visible should make it easier to notice anomalies and opportunities. To use +this script on Windows just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat. + +On Linux you can get autoninja to invoke this script using this syntax: + +$ NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default/ chrome + +You can also call this script directly using ninja's syntax to specify the +output directory of interest: + +> python3 post_build_ninja_summary.py -C out/Default + +Typical output looks like this: + +>ninja -C out\debug_component base +ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp +ninja: Entering directory `out\debug_component' +[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files +Longest build steps: + 0.1 weighted s to build obj/base/base/trace_log.obj (6.7 s elapsed time) + 0.2 weighted s to build nasm.exe, nasm.exe.pdb (0.2 s elapsed time) + 0.3 weighted s to build obj/base/base/win_util.obj (12.4 s elapsed time) + 1.2 weighted s to build base.dll, base.dll.lib (1.2 s elapsed time) +Time by build-step type: + 0.0 s weighted time to generate 6 .lib files (0.3 s elapsed time sum) + 0.1 s weighted time to generate 25 .stamp files (1.2 s elapsed time sum) + 0.2 s weighted time to generate 20 .o files (2.8 s elapsed time sum) + 1.7 s weighted time to generate 4 PEFile (linking) files (2.0 s elapsed +time sum) + 23.9 s weighted time to generate 770 .obj files (974.8 s elapsed time sum) +26.1 s weighted time (982.9 s elapsed time sum, 37.7x parallelism) +839 build steps completed, average of 32.17/s + +If no gn clean has been done then results will be for the last non-NULL +invocation of ninja. Ideas for future statistics, and implementations are +appreciated. + +The "weighted" time is the elapsed time of each build step divided by the number +of tasks that were running in parallel. This makes it an excellent approximation +of how "important" a slow step was. A link that is entirely or mostly serialized +will have a weighted time that is the same or similar to its elapsed time. A +compile that runs in parallel with 999 other compiles will have a weighted time +that is tiny.""" + +import argparse +import errno +import fnmatch +import os +import subprocess +import sys + +# The number of long build times to report: +long_count = 10 +# The number of long times by extension to report +long_ext_count = 10 + + +class Target: + """Represents a single line read for a .ninja_log file.""" + + def __init__(self, start, end): + """Creates a target object by passing in the start/end times in seconds + as a float.""" + self.start = start + self.end = end + # A list of targets, appended to by the owner of this object. + self.targets = [] + self.weighted_duration = 0.0 + + def Duration(self): + """Returns the task duration in seconds as a float.""" + return self.end - self.start + + def SetWeightedDuration(self, weighted_duration): + """Sets the duration, in seconds, passed in as a float.""" + self.weighted_duration = weighted_duration + + def WeightedDuration(self): + """Returns the task's weighted duration in seconds as a float. + + Weighted_duration takes the elapsed time of the task and divides it + by how many other tasks were running at the same time. Thus, it + represents the approximate impact of this task on the total build time, + with serialized or serializing steps typically ending up with much + longer weighted durations. + weighted_duration should always be the same or shorter than duration. + """ + # Allow for modest floating-point errors + epsilon = 0.000002 + if self.weighted_duration > self.Duration() + epsilon: + print("%s > %s?" % (self.weighted_duration, self.Duration())) + assert self.weighted_duration <= self.Duration() + epsilon + return self.weighted_duration + + def DescribeTargets(self): + """Returns a printable string that summarizes the targets.""" + # Some build steps generate dozens of outputs - handle them sanely. + # The max_length was chosen so that it can fit most of the long + # single-target names, while minimizing word wrapping. + result = ", ".join(self.targets) + max_length = 65 + if len(result) > max_length: + result = result[:max_length] + "..." + return result + + +# Copied with some modifications from ninjatracing +def ReadTargets(log, show_all): + """Reads all targets from .ninja_log file |log_file|, sorted by duration. + + The result is a list of Target objects.""" + header = log.readline() + # Handle empty ninja_log gracefully by silently returning an empty list of + # targets. + if not header: + return [] + assert header == "# ninja log v6\n", "unrecognized ninja log version %r" % header + targets_dict = {} + last_end_seen = 0.0 + for line in log: + parts = line.strip().split("\t") + if len(parts) != 5: + # If ninja.exe is rudely halted then the .ninja_log file may be + # corrupt. Silently continue. + continue + start, end, _, name, cmdhash = parts # Ignore restat. + # Convert from integral milliseconds to float seconds. + start = int(start) / 1000.0 + end = int(end) / 1000.0 + if not show_all and end < last_end_seen: + # An earlier time stamp means that this step is the first in a new + # build, possibly an incremental build. Throw away the previous + # data so that this new build will be displayed independently. + # This has to be done by comparing end times because records are + # written to the .ninja_log file when commands complete, so end + # times are guaranteed to be in order, but start times are not. + targets_dict = {} + target = None + if cmdhash in targets_dict: + target = targets_dict[cmdhash] + if not show_all and (target.start != start or target.end != end): + # If several builds in a row just run one or two build steps + # then the end times may not go backwards so the last build may + # not be detected as such. However in many cases there will be a + # build step repeated in the two builds and the changed + # start/stop points for that command, identified by the hash, + # can be used to detect and reset the target dictionary. + targets_dict = {} + target = None + if not target: + targets_dict[cmdhash] = target = Target(start, end) + last_end_seen = end + target.targets.append(name) + return list(targets_dict.values()) + + +def GetExtension(target, extra_patterns): + """Return the file extension that best represents a target. + + For targets that generate multiple outputs it is important to return a + consistent 'canonical' extension. Ultimately the goal is to group build steps + by type.""" + for output in target.targets: + if extra_patterns: + for fn_pattern in extra_patterns.split(";"): + if fnmatch.fnmatch(output, "*" + fn_pattern + "*"): + return fn_pattern + # Not a true extension, but a good grouping. + if output.endswith("type_mappings"): + extension = "type_mappings" + break + + # Capture two extensions if present. For example: file.javac.jar should + # be distinguished from file.interface.jar. + root, ext1 = os.path.splitext(output) + _, ext2 = os.path.splitext(root) + extension = ext2 + ext1 # Preserve the order in the file name. + + if len(extension) == 0: + extension = "(no extension found)" + + if ext1 in [".pdb", ".dll", ".exe"]: + extension = "PEFile (linking)" + # Make sure that .dll and .exe are grouped together and that the + # .dll.lib files don't cause these to be listed as libraries + break + if ext1 in [".so", ".TOC"]: + extension = ".so (linking)" + # Attempt to identify linking, avoid identifying as '.TOC' + break + # Make sure .obj files don't get categorized as mojo files + if ext1 in [".obj", ".o"]: + break + # Jars are the canonical output of java targets. + if ext1 == ".jar": + break + # Normalize all mojo related outputs to 'mojo'. + if output.count(".mojom") > 0: + extension = "mojo" + break + return extension + + +def SummarizeEntries(entries, extra_step_types, elapsed_time_sorting): + """Print a summary of the passed in list of Target objects.""" + + # Create a list that is in order by time stamp and has entries for the + # beginning and ending of each build step (one time stamp may have multiple + # entries due to multiple steps starting/stopping at exactly the same time). + # Iterate through this list, keeping track of which tasks are running at all + # times. At each time step calculate a running total for weighted time so + # that when each task ends its own weighted time can easily be calculated. + task_start_stop_times = [] + + earliest = -1 + latest = 0 + total_cpu_time = 0 + for target in entries: + if earliest < 0 or target.start < earliest: + earliest = target.start + if target.end > latest: + latest = target.end + total_cpu_time += target.Duration() + task_start_stop_times.append((target.start, "start", target)) + task_start_stop_times.append((target.end, "stop", target)) + length = latest - earliest + weighted_total = 0.0 + + # Sort by the time/type records and ignore |target| + task_start_stop_times.sort(key=lambda times: times[:2]) + # Now we have all task start/stop times sorted by when they happen. If a + # task starts and stops on the same time stamp then the start will come + # first because of the alphabet, which is important for making this work + # correctly. + # Track the tasks which are currently running. + running_tasks = {} + # Record the time we have processed up to so we know how to calculate time + # deltas. + last_time = task_start_stop_times[0][0] + # Track the accumulated weighted time so that it can efficiently be added + # to individual tasks. + last_weighted_time = 0.0 + # Scan all start/stop events. + for event in task_start_stop_times: + time, action_name, target = event + # Accumulate weighted time up to now. + num_running = len(running_tasks) + if num_running > 0: + # Update the total weighted time up to this moment. + last_weighted_time += (time - last_time) / float(num_running) + if action_name == "start": + # Record the total weighted task time when this task starts. + running_tasks[target] = last_weighted_time + if action_name == "stop": + # Record the change in the total weighted task time while this task + # ran. + weighted_duration = last_weighted_time - running_tasks[target] + target.SetWeightedDuration(weighted_duration) + weighted_total += weighted_duration + del running_tasks[target] + last_time = time + assert len(running_tasks) == 0 + + # Warn if the sum of weighted times is off by more than half a second. + if abs(length - weighted_total) > 500: + print( + "Warning: Possible corrupt ninja log, results may be " + "untrustworthy. Length = %.3f, weighted total = %.3f" + % (length, weighted_total) + ) + + # Print the slowest build steps: + print(" Longest build steps:") + if elapsed_time_sorting: + entries.sort(key=lambda x: x.Duration()) + else: + entries.sort(key=lambda x: x.WeightedDuration()) + for target in entries[-long_count:]: + print( + " %8.1f weighted s to build %s (%.1f s elapsed time)" + % (target.WeightedDuration(), target.DescribeTargets(), target.Duration()) + ) + + # Sum up the time by file extension/type of the output file + count_by_ext = {} + time_by_ext = {} + weighted_time_by_ext = {} + # Scan through all of the targets to build up per-extension statistics. + for target in entries: + extension = GetExtension(target, extra_step_types) + time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration() + weighted_time_by_ext[extension] = ( + weighted_time_by_ext.get(extension, 0) + target.WeightedDuration() + ) + count_by_ext[extension] = count_by_ext.get(extension, 0) + 1 + + print(" Time by build-step type:") + # Copy to a list with extension name and total time swapped, to (time, ext) + if elapsed_time_sorting: + weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in time_by_ext.items()) + else: + weighted_time_by_ext_sorted = sorted( + (y, x) for (x, y) in weighted_time_by_ext.items() + ) + # Print the slowest build target types: + for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]: + print( + " %8.1f s weighted time to generate %d %s files " + "(%1.1f s elapsed time sum)" + % (time, count_by_ext[extension], extension, time_by_ext[extension]) + ) + + print( + " %.1f s weighted time (%.1f s elapsed time sum, %1.1fx " + "parallelism)" % (length, total_cpu_time, total_cpu_time * 1.0 / length) + ) + print( + " %d build steps completed, average of %1.2f/s" + % (len(entries), len(entries) / (length)) + ) + + +def main(): + log_file = ".ninja_log" + metrics_file = "siso_metrics.json" + parser = argparse.ArgumentParser() + parser.add_argument("-C", dest="build_directory", help="Build directory.") + parser.add_argument( + "-s", + "--step-types", + help="semicolon separated fnmatch patterns for build-step grouping", + ) + parser.add_argument( + "-e", + "--elapsed_time_sorting", + default=False, + action="store_true", + help="Sort output by elapsed time instead of weighted time", + ) + parser.add_argument("--log-file", help="specific ninja log file to analyze.") + args, _extra_args = parser.parse_known_args() + if args.build_directory: + log_file = os.path.join(args.build_directory, log_file) + metrics_file = os.path.join(args.build_directory, metrics_file) + if args.log_file: + log_file = args.log_file + if not args.step_types: + # Offer a convenient way to add extra step types automatically, + # including when this script is run by autoninja. get() returns None if + # the variable isn't set. + args.step_types = os.environ.get("chromium_step_types") + if args.step_types: + # Make room for the extra build types. + global long_ext_count + long_ext_count += len(args.step_types.split(";")) + + if os.path.exists(metrics_file): + # Automatically handle summarizing siso builds. + cmd = ["siso.bat" if "win32" in sys.platform else "siso"] + cmd.extend(["metrics", "summary"]) + if args.build_directory: + cmd.extend(["-C", args.build_directory]) + if args.step_types: + cmd.extend(["--step_types", args.step_types]) + if args.elapsed_time_sorting: + cmd.append("--elapsed_time_sorting") + subprocess.run(cmd) + else: + try: + with open(log_file, "r") as log: + entries = ReadTargets(log, False) + if entries: + SummarizeEntries( + entries, args.step_types, args.elapsed_time_sorting + ) + except IOError: + print("Log file %r not found, no build summary created." % log_file) + return errno.ENOENT + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cccl_upstream/ci/nvrtc_libcudacxx.sh b/cccl_upstream/ci/nvrtc_libcudacxx.sh new file mode 100755 index 00000000..3959241d --- /dev/null +++ b/cccl_upstream/ci/nvrtc_libcudacxx.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + + +PRESET="libcudacxx-nvrtc" +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") + +configure_and_build_preset "libcudacxx NVRTC" "$PRESET" "${CMAKE_OPTIONS[@]}" + +sccache -z > /dev/null || : +test_preset "libcudacxx NVRTC" "${PRESET}" +sccache --show-adv-stats || : diff --git a/cccl_upstream/ci/pretty_printing.sh b/cccl_upstream/ci/pretty_printing.sh new file mode 100644 index 00000000..ab0f2c8b --- /dev/null +++ b/cccl_upstream/ci/pretty_printing.sh @@ -0,0 +1,122 @@ +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Print "ARG=${ARG}" for all args. +function print_var_values() { + # Iterate through the arguments + for var_name in "$@"; do + if [[ -z "$var_name" ]]; then + echo "Usage: print_var_values ..." + return 1 + fi + + # Dereference the variable and print the result + echo "$var_name=${!var_name:-(undefined)}" + done +} + +# begin_group: Start a named section of log output, possibly with color. +# Usage: begin_group "Group Name" [Color] +# Group Name: A string specifying the name of the group. +# Color (optional): ANSI color code to set text color. Default is blue (1;34). +function begin_group() { + # See options for colors here: https://gist.github.com/JBlond/2fea43a3049b38287e5e9cefc87b2124 + local blue="34" + local name="${1:-}" + local color="${2:-$blue}" + + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo -e "::group::\e[${color}m${name}\e[0m" + else + echo -e "\e[${color}m================== ${name} ======================\e[0m" + fi +} + +# end_group: End a named section of log output and print status based on exit status. +# Usage: end_group "Group Name" [Exit Status] +# Group Name: A string specifying the name of the group. +# Exit Status (optional): The exit status of the command run within the group. Default is 0. +function end_group() { + local name="${1:-}" + local build_status="${2:-0}" + local duration="${3:-}" + local red="31" + local blue="34" + + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::endgroup::" + + if [[ "$build_status" -ne 0 ]]; then + echo -e "::error::\e[${red}m ${name} - Failed (⬆️ click above for full log ⬆️)\e[0m" + fi + else + if [[ "$build_status" -ne 0 ]]; then + echo -e "\e[${red}m================== End ${name} - Failed${duration:+ - Duration: ${duration}s} ==================\e[0m" + else + echo -e "\e[${blue}m================== End ${name} - Success${duration:+ - Duration: ${duration}s} ==================\n\e[0m" + fi + fi +} + +declare -A command_durations + +# Runs a command within a named group, handles the exit status, and prints appropriate messages based on the result. +# Usage: run_command "Group Name" command [arguments...] +function run_command() { + local group_name="${1:-}" + shift + local command=("$@") + local status + + begin_group "$group_name" + echo "Working directory: $(pwd)" + echo "Running command: ${command[*]}" + set +e + local start_time + start_time=$(date +%s) + # If RUN_COMMAND_RETRY_PARAMS is set to " ", use retry.sh to run the command: + if [[ -v RUN_COMMAND_RETRY_PARAMS && "${#RUN_COMMAND_RETRY_PARAMS[@]}" -eq 2 ]]; then + status=0 + "$ci_dir/util/retry.sh" "${RUN_COMMAND_RETRY_PARAMS[@]}" "${command[@]}" || status=$? + else + status=0 + "${command[@]}" || status=$? + fi + local end_time + end_time=$(date +%s) + set -e + local duration=$((end_time - start_time)) + end_group "$group_name" "$status" "$duration" + command_durations["$group_name"]=$duration + return "$status" +} + +function string_width() { + local str="$1" + echo "$str" | awk '{print length}' +} + +function print_time_summary() { + local max_length=0 + local group + + # Find the longest group name for formatting + for group in "${!command_durations[@]}"; do + local group_length + group_length=$(echo "$group" | awk '{print length}') + if [[ "$group_length" -gt "$max_length" ]]; then + max_length=$group_length + fi + done + + if [[ "$max_length" -eq 0 ]]; then + return + fi + + echo "Time Summary:" + for group in "${!command_durations[@]}"; do + printf "%-${max_length}s : %s seconds\n" "$group" "${command_durations[$group]}" + done + + # Clear the array of timing info + declare -gA command_durations=() +} diff --git a/cccl_upstream/ci/project_files_and_dependencies.yaml b/cccl_upstream/ci/project_files_and_dependencies.yaml new file mode 100644 index 00000000..b3a18356 --- /dev/null +++ b/cccl_upstream/ci/project_files_and_dependencies.yaml @@ -0,0 +1,287 @@ +# Configuration for CCCL project change detection. +# Projects are declared in this file along with their human-friendly names, path +# matching patterns, and dependency relationships. Every path pattern is +# evaluated as a regular expression anchored to the repository root. +# +# Changed files that satisfy a project's include_regexes, exclude_regexes, and +# exclude_project_files checks will mark that project as "dirty". The current project +# will be added to the FULL_BUILD if it has dirty files or if any of its +# full_dependencies are dirty. +# +# Additionally, if any of a project's lite_dependencies are dirty, the current project +# will be added to the LITE_BUILD. +# +# If any transitive dependencies are dirty (eg. a full or lite dependency has +# dirty dependencies of its own), the current project will also be added to the LITE_BUILD. +# +# Projects with large dependency trees are split into "public" and "internal" sub-projects. +# The "public" sub-project contains only the public API files, while the "internal" +# sub-project contains tests, examples, and other non-public files. This allows changes to +# internal files to avoid triggering rebuilds of dependent projects when the public API +# is not changed. +# +# Layout: +# project_key: mapping from project key to configuration +# name: display name used in logs/reports +# matrix_project: project name to use for matrix generation. If not provided, +# the project is considered internal-only and will not be listed +# in the FULL_BUILD or LITE_BUILD outputs. +# lite_dependencies: Add the current project to the `lite` matrix if any of these +# dependencies are dirty. +# full_dependencies: Add the current project to the `full` matrix if any of these +# dependencies are dirty. +# include_regexes: regexes that match files for this project +# exclude_regexes: regexes that exclude files from the included set +# exclude_project_files: list of other project keys whose files should be +# excluded from this project + +projects: + # `core` collects any dirty files that are not matched by other projects. It + # never appears in dependency lists because the script treats it specially, + # triggering a full rebuild of all projects when any core file is dirty. + core: + name: "CCCL Infrastructure" + + libcudacxx_public: + name: "libcu++ Public API" + lite_dependencies: ['thrust_public', 'cub_public'] + full_dependencies: [] + include_regexes: ["libcudacxx/include/"] + + libcudacxx_internal: + name: "libcu++ Tests/Infra" + matrix_project: "libcudacxx" + lite_dependencies: [c2h] + full_dependencies: [libcudacxx_public] + include_regexes: ["libcudacxx/"] + exclude_project_files: [libcudacxx_public] + + cub_public: + name: "CUB Public API" + lite_dependencies: [libcudacxx_public, thrust_public] + full_dependencies: [] + include_regexes: ["cub/cub/"] + + cub_internal: + name: "CUB Tests/Infra" + matrix_project: "cub" + lite_dependencies: [c2h, nvbench_helper] + full_dependencies: [cub_public] + include_regexes: ["cub/"] + exclude_project_files: [cub_public] + + thrust_public: + name: "Thrust Public API" + lite_dependencies: [libcudacxx_public, cub_public] + full_dependencies: [] + include_regexes: ["thrust/thrust/"] + + thrust_internal: + name: "Thrust Tests/Infra" + matrix_project: "thrust" + lite_dependencies: [nvbench_helper] + full_dependencies: [thrust_public] + include_regexes: ["thrust/"] + exclude_project_files: [thrust_public] + + cudax_public: + name: "CUDA Experimental Public API" + lite_dependencies: [libcudacxx_public, thrust_public, cub_public] + full_dependencies: [] + include_regexes: ["cudax/include/"] + + cudax_internal: + name: "CUDA Experimental Tests/Infra" + matrix_project: "cudax" + lite_dependencies: [c2h, nvbench_helper] + full_dependencies: [cudax_public] + include_regexes: ["cudax/"] + exclude_project_files: [cudax_public] + + cccl_c_parallel_public: + name: "CCCL C Parallel Library Public API" + lite_dependencies: [libcudacxx_public, cub_public, thrust_public] + full_dependencies: [] + include_regexes: + - "c/parallel/include/" + - "c/parallel/src/" + + cccl_c_parallel_internal: + name: "CCCL C Parallel Library Tests/Infra" + matrix_project: "cccl_c_parallel" + lite_dependencies: [c2h] + full_dependencies: [cccl_c_parallel_public] + include_regexes: ["c/parallel/"] + exclude_project_files: [cccl_c_parallel_public] + + cccl_c_parallel_v2: + name: "CCCL C Parallel Library v2 (HostJIT)" + matrix_project: "cccl_c_parallel_v2" + # v2 depends on libcudacxx, cub, and thrust headers (it JIT-compiles + # CUB's host+device code via HostJIT). Any change to those should trigger + # v2 to run. + lite_dependencies: [libcudacxx_public, cub_public, thrust_public, c2h] + full_dependencies: [] + include_regexes: + - "c/parallel\\.v2/" + + python_v2: + name: "Python (cuda.compute on v2/HostJIT)" + matrix_project: "python_v2" + # cccl_c_parallel_v2 already pulls in libcudacxx/cub/thrust, so listing + # it here transitively triggers python_v2 on any of those upstream + # changes too. Direct includes catch Python-only edits. + lite_dependencies: [cccl_c_parallel_v2] + full_dependencies: [] + include_regexes: + - "python/cuda_cccl/" + - "pyproject\\.toml" + + python_tsan: + name: "Python (cuda.compute free-threaded ThreadSanitizer)" + matrix_project: "python_tsan" + # Same v1 sources as `python`, rebuilt with ThreadSanitizer. Must be listed + # here or the per-PR python_tsan job is pruned as never-dirty and never runs. + # cccl_c_parallel_public covers c/parallel/{src,include}; c/parallel/CMakeLists.txt + # is listed directly because the TSan build config (the -fsanitize=thread option) + # lives there and is owned by cccl_c_parallel_internal, which this lane otherwise + # does not depend on -- without it a change to the TSan flags would skip this lane. + lite_dependencies: [cccl_c_parallel_public] + full_dependencies: [] + include_regexes: + - "python/cuda_cccl/" + - "pyproject\\.toml" + - "c/parallel/CMakeLists\\.txt" + + cccl_c_stf: + name: "CCCL C CUDASTF Library" + matrix_project: "cccl_c_stf" + lite_dependencies: [libcudacxx_public, cudax_public, c2h] + full_dependencies: [] + include_regexes: ["c/experimental/stf/"] + exclude_regexes: [] + + python: + name: "Python" + matrix_project: "python" + lite_dependencies: [cccl_c_parallel_public] + full_dependencies: [] + include_regexes: + - "python/" + - "pyproject.toml" + + packaging: + name: "CCCL Packaging" + matrix_project: "packaging" + # Anything that affects CMake packages / install rules: + lite_dependencies: + - libcudacxx_internal + - libcudacxx_public + - cub_internal + - cub_public + - thrust_internal + - thrust_public + - cudax_internal + - cudax_public + full_dependencies: [] + include_regexes: + - "examples/" + - "test/cmake/" + - "ci/test/" + + stdpar: + name: "stdpar" + matrix_project: "stdpar" + lite_dependencies: [thrust_public] + full_dependencies: [] + include_regexes: ["test/stdpar/"] + + c2h: + name: "Catch2Helper" + lite_dependencies: [libcudacxx_public, cub_public, thrust_public] + full_dependencies: [] + include_regexes: ["c2h/"] + + nvbench_helper: + name: "NVBench Helper" + matrix_project: "nvbench_helper" + lite_dependencies: [libcudacxx_public, cub_public, thrust_public] + full_dependencies: [] + include_regexes: + - "nvbench_helper/" + # CCCLBenchmarkRegistry.cmake is effectively part of nvbench_helper: + - "benchmarks/cmake/" + + nvrtcc: + name: "nvrtcc" + matrix_project: "nvrtcc" + lite_dependencies: [] + full_dependencies: [] + include_regexes: ["nvrtcc/"] + + # This is a dummy project, and only really serves to encode dependencies. Any time any + # of the C/C++ projects are modified, we want to launch one (and only one) clang-tidy + # job for the whole of CCCL. + tidy: + name: "clang-tidy" + matrix_project: "tidy" + full_dependencies: + - libcudacxx_public + - libcudacxx_internal + - cub_public + - cub_internal + - thrust_public + - thrust_internal + - cudax_public + - cudax_internal + - cccl_c_parallel_public + - cccl_c_parallel_internal + - cccl_c_parallel_v2 + - cccl_c_stf + - stdpar + - c2h + - nvrtcc + include_regexes: [".clang-tidy"] + +# Files matching any of these regexes will be ignored globally. +ignore_regexes: + - '.+\.md$' + - '\.branch_notes/' + - '\.coderabbit\.yaml' + - '\.clang-format' + - '\.clangd' + # Do not add .clang-tidy to this list. If we modify .clang-tidy to either add or remove + # a check we still want CI to run the clang-tidy job even if it results in no C/C++ code + # changes. + # + # - '\.clang-tidy' + - '\.devcontainer/img' + - '\.git-blame-ignore-revs' + - '\.github/actions/docs-build' + - '\.github/CODEOWNERS' + - '\.github/copy-pr-bot\.yaml' + - '\.github/ISSUE_TEMPLATE/' + - '\.github/problem-matchers/problem-matcher\.json' + - '\.github/workflows/backport-prs\.yml' + - '\.github/workflows/bench.*\.yml' + - '\.github/workflows/compile-time-bench\.yml' + - '\.github/workflows/build-docs\.yml' + - '\.github/workflows/build-matx\.yml' + - '\.github/workflows/build-pytorch\.yml' + - '\.github/workflows/build-rapids\.yml' + - '\.github/workflows/git-bisect\.yml' + - '\.github/workflows/project_automation.*\.yml' + - '\.github/workflows/release.*\.yml' + - '\.github/workflows/triage_rotation\.yml' + - '\.github/workflows/update_branch_version\.yml' + - '\.github/workflows/verify-devcontainers\.yml' + - '\.gitignore' + - "benchmarks/scripts/" # Python+bash scripts for running benchmarks, not used in tests. + - 'cccl-version.json' + - 'ci/bench/' + - 'ci/bench.+yaml' + - 'ci/matx/' + - 'ci/pytorch/' + - 'ci/rapids/' + - 'docs/' + - 'LICENSE' diff --git a/cccl_upstream/ci/pyenv_helper.sh b/cccl_upstream/ci/pyenv_helper.sh new file mode 100644 index 00000000..fffe9edc --- /dev/null +++ b/cccl_upstream/ci/pyenv_helper.sh @@ -0,0 +1,77 @@ +setup_python_env() { + local py_version=$1 + + # Source pretty_printing.sh for begin_group/end_group helpers + local script_dir + script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + # shellcheck source=ci/pretty_printing.sh + source "${script_dir}/pretty_printing.sh" + + begin_group "🐍 Setting up Python ${py_version} (uv)" + + # Install uv if not present + if ! command -v uv &> /dev/null; then + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + fi + + # Create a venv with the requested Python version. + # uv downloads a pre-built CPython binary automatically — no compilation needed. + uv venv --seed --python "${py_version}" "${HOME}/.cccl-venv" + + # Windows venvs use Scripts/, Linux/macOS use bin/ + if [[ -f "${HOME}/.cccl-venv/Scripts/activate" ]]; then + #shellcheck disable=SC1091 + source "${HOME}/.cccl-venv/Scripts/activate" + else + #shellcheck disable=SC1091 + source "${HOME}/.cccl-venv/bin/activate" + fi + + end_group "🐍 Setting up Python ${py_version} (uv)" +} + +# Pin the cuda-toolkit wheels to the container's CTK major.minor (read from nvcc) +# via PIP_CONSTRAINT when the mode ($1) is "pinned" (the default; empty also means +# pinned). "latest" and "sysctk" leave it unpinned; any other value is a hard +# error. This is the lane's mode gate -- it runs before ctk_extra_flavor in every +# script, so ctk_extra_flavor can assume the mode is already valid. Also sets and +# exports cuda_version / cuda_major_version; the caller uses cuda_major_version in +# the pip-extra name (e.g. minimal-cu${cuda_major_version}). +pin_cuda_toolkit() { + cuda_version=$(nvcc --version | grep release | awk '{print $6}' | tr -d ',' | cut -d '.' -f 1-2 | cut -d 'V' -f 2) + cuda_major_version=$(echo "$cuda_version" | cut -d '.' -f 1) + export cuda_version cuda_major_version + + local mode="${1:-pinned}" + case "${mode,,}" in + pinned) + export PIP_CONSTRAINT="${TMPDIR:-/tmp}/ctk-constraint.txt" + echo "cuda-toolkit==${cuda_version}.*" > "${PIP_CONSTRAINT}" + ;; + latest | sysctk) + # No pin. Clear any inherited constraint so it cannot affect the + # resolve (latest tests the newest minor; sysctk installs no + # cuda-toolkit wheel at all). + unset PIP_CONSTRAINT + ;; + *) + echo "ERROR: invalid ctk mode '${mode}' (expected pinned|latest|sysctk)" >&2 + return 1 + ;; + esac +} + +# Echoes the pip-extra toolkit "flavor" for the mode ($1): "sysctk" when the mode +# is sysctk (rely on the system-provided CUDA toolkit) or "cu" otherwise +# (pip-installed toolkit). The mode is validated by pin_cuda_toolkit, which every +# lane calls first. Combine with the CUDA major, e.g. +# "minimal-$(ctk_extra_flavor "${ctk_mode}")${cuda_major_version}" -> minimal-sysctk12. +ctk_extra_flavor() { + local mode="${1:-}" + if [[ "${mode,,}" == "sysctk" ]]; then + echo "sysctk" + else + echo "cu" + fi +} diff --git a/cccl_upstream/ci/pytorch/build_pytorch.sh b/cccl_upstream/ci/pytorch/build_pytorch.sh new file mode 100755 index 00000000..fc08c8c2 --- /dev/null +++ b/cccl_upstream/ci/pytorch/build_pytorch.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly pytorch_repo=https://github.com/pytorch/pytorch.git +readonly pytorch_branch=main + +# Ensure the script is being executed in the root cccl directory: +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../.."; +readonly cccl_repo="${PWD}" + +log_vars() { + for var in "$@"; do + echo "${var}=${!var}" + done +} + +# Define CCCL_TAG to override the default CCCL SHA. Otherwise the current HEAD of the local checkout is used. +echo "CCCL_TAG (override): ${CCCL_TAG-}"; +if test -n "${CCCL_TAG-}"; then + # If CCCL_TAG is defined, fetch it to the local checkout + git -C "${cccl_repo}" fetch origin "${CCCL_TAG}"; + cccl_sha="$(git -C "${cccl_repo}" rev-parse FETCH_HEAD)"; +else + cccl_sha="$(git -C "${cccl_repo}" rev-parse HEAD)"; +fi + +readonly workdir="${cccl_repo}/build/${CCCL_BUILD_INFIX:-}/pytorch" + +log_vars \ + pytorch_repo pytorch_branch \ + cccl_repo cccl_sha \ + workdir + +mkdir -p "${workdir}" +cd "${workdir}" +echo "Working in ${workdir}" + +echo "::group::Cloning CCCL..." +rm -rf cccl +git clone "${cccl_repo}" +git -C cccl checkout "${cccl_sha}" +echo "CCCL HEAD:" +git -C cccl log -1 --format=short +echo "::endgroup::" + +# Setup a CUDA environment with the requested CCCL. +# Use a local directory to avoid modifying the actual CUDA install: +echo "::group::Setting up clone of CUDA environment with custom CCCL..." +( + set -x + rm -rf ./cuda + cp -Hr /usr/local/cuda ./cuda + rm -rf ./cuda/include/cccl/* + cccl/ci/install_cccl.sh ./cccl-install > /dev/null + cp -r ./cccl-install/include/* ./cuda/include/cccl +) +export PATH="$PWD/cuda/bin:$PATH" +export CUDA_HOME="$PWD/cuda" +export CUDA_PATH="$PWD/cuda" +command -v nvcc +nvcc --version +echo "::endgroup::" + +echo "::group::Cloning PyTorch..." +rm -rf pytorch +git clone "${pytorch_repo}" -b "${pytorch_branch}" --recursive --depth 1 +echo "PyTorch HEAD:" +git -C pytorch log -1 --format=short +echo "::endgroup::" + +echo "::group::Installing PyTorch build dependencies..." +pytorch_root="$PWD/pytorch" +export PYTHONPATH="${pytorch_root}:${pytorch_root}/tools:${PYTHONPATH:-}" +pip install -r "${pytorch_root}/requirements-build.txt" +echo "::endgroup::" + +echo "::group::Configuring PyTorch..." +rm -rf build +mkdir build +declare -a cmake_args=( + "-DUSE_NCCL=OFF" + # Need to define this explicitly, torch's FindCUDA logic adds ancient arches if left undefined: + "-DTORCH_CUDA_ARCH_LIST=7.5;8.0;9.0;10.0;12.0" +) +SCCACHE_NO_DIST_COMPILE=1 cmake -S ./pytorch -B ./build -G Ninja "${cmake_args[@]}" +echo "::endgroup::" + +# Verify that the configured build is using the custom CUDA dir for CTK and nvcc: +if ! grep -q "CUDA_TOOLKIT_ROOT_DIR:PATH=$PWD/cuda" ./build/CMakeCache.txt; then + echo "Error: CUDA_TOOLKIT_ROOT_DIR does not point to the custom CUDA"; + exit 1; +fi +if ! grep -q "CUDA_NVCC_EXECUTABLE:FILEPATH=$PWD/cuda/bin/nvcc" ./build/CMakeCache.txt; then + echo "Error: CUDA_NVCC_EXECUTABLE does not point to the custom CUDA"; + exit 1; +fi + +# This builds a bunch of unnecessary targets. Leaving here to use as a fallback if the +# ninja target extraction below starts failing: +# echo "::group::Building torch_cuda target..." +# cmake --build ./build/ --target torch_cuda +# echo "::endgroup::" + +# This cuts the number of built targets roughly in half: +echo "::group::Extracting cuda targets from build.ninja..." +# Query ninja for all object files built from CUDA source files +# that are part of the torch_cuda library: +ninja -C ./build -t query lib/libtorch_cuda.so | + grep -E "torch_cuda\\.dir/.*\\.cu\\.o$" | + sort | uniq | tee build/cuda_targets.txt +# At the time this script was written, there were 311 cuda targets. +# Check that there are at least 100 detected targets, otherwise fail. +num_targets=$(wc -l < build/cuda_targets.txt) +if test "$num_targets" -lt 100; then + echo "Error: extracted cuda targets count is less than 100! ($num_targets)"; + echo "This likely indicates a failure to extract the targets from ninja."; + exit 1; +fi +echo "::endgroup::" + +echo "::group::Building $num_targets pytorch CUDA targets with custom CCCL..." +torch_cuda_targets="$(xargs -a build/cuda_targets.txt)" +declare -a torch_cuda_targets="($torch_cuda_targets)" + +time ninja -C ./build "${torch_cuda_targets[@]}" "-j${PARALLEL_LEVEL:-}" + +echo "::endgroup::" + +echo "PyTorch CUDA targets built successfully with custom CCCL." diff --git a/cccl_upstream/ci/rapids/cuda13.3-conda/devcontainer.json b/cccl_upstream/ci/rapids/cuda13.3-conda/devcontainer.json new file mode 100644 index 00000000..6c138a8e --- /dev/null +++ b/cccl_upstream/ci/rapids/cuda13.3-conda/devcontainer.json @@ -0,0 +1,124 @@ +{ + "image": "rapidsai/devcontainers:26.10-cpp-mambaforge", + "runArgs": [ + "--init", + "--rm", + "--name", + "${localEnv:USER:anon}-${localWorkspaceFolderBasename}-rapids-26.10-cuda13.3-conda", + "--ulimit", + "nofile=500000" + ], + "hostRequirements": { + "gpu": "optional" + }, + "containerEnv": { + "AWS_ROLE_ARN": "arn:aws:iam::279114543810:role/nv-gha-token-sccache-devs", + "CI": "${localEnv:CI}", + "CUDA_VERSION": "13.3", + "CUDAARCHS": "75-real", + "DEFAULT_CONDA_ENV": "rapids", + "DEVCONTAINER_UTILS_ENABLE_SCCACHE_DIST": "true", + "HISTFILE": "/home/coder/.cache/._bash_history", + "INFER_NUM_DEVICE_ARCHITECTURES": "1", + "LIBCUDF_KERNEL_CACHE_PATH": "/home/coder/cudf/cpp/build/latest/jitify_cache", + "MAX_DEVICE_OBJ_TO_COMPILE_IN_PARALLEL": "20", + "PYTHON_PACKAGE_MANAGER": "conda", + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONSAFEPATH": "1", + "PYTHONUNBUFFERED": "1", + "RAPIDS_cmake_GIT_REPO": "${localEnv:RAPIDS_cmake_GIT_REPO}", + "RAPIDS_cudf_GIT_REPO": "${localEnv:RAPIDS_cudf_GIT_REPO}", + "RAPIDS_cugraph_GIT_REPO": "${localEnv:RAPIDS_cugraph_GIT_REPO}", + "RAPIDS_cugraph_gnn_GIT_REPO": "${localEnv:RAPIDS_cugraph_gnn_GIT_REPO}", + "RAPIDS_cuml_GIT_REPO": "${localEnv:RAPIDS_cuml_GIT_REPO}", + "RAPIDS_cuopt_GIT_REPO": "${localEnv:RAPIDS_cuopt_GIT_REPO}", + "RAPIDS_cuvs_GIT_REPO": "${localEnv:RAPIDS_cuvs_GIT_REPO}", + "RAPIDS_kvikio_GIT_REPO": "${localEnv:RAPIDS_kvikio_GIT_REPO}", + "RAPIDS_LIBS": "${localEnv:RAPIDS_LIBS}", + "RAPIDS_nvforest_GIT_REPO": "${localEnv:RAPIDS_nvforest_GIT_REPO}", + "RAPIDS_raft_GIT_REPO": "${localEnv:RAPIDS_raft_GIT_REPO}", + "RAPIDS_rmm_GIT_REPO": "${localEnv:RAPIDS_rmm_GIT_REPO}", + "RAPIDS_ucxx_GIT_REPO": "${localEnv:RAPIDS_ucxx_GIT_REPO}", + "SCCACHE_BUCKET": "rapids-sccache-devs", + "SCCACHE_DIST_FALLBACK_TO_LOCAL_COMPILE": "${localEnv:SCCACHE_DIST_FALLBACK_TO_LOCAL_COMPILE:true}", + "SCCACHE_DIST_MAX_RETRIES": "${localEnv:SCCACHE_DIST_MAX_RETRIES:4}", + "SCCACHE_DIST_REQUEST_TIMEOUT": "${localEnv:SCCACHE_DIST_REQUEST_TIMEOUT:7140}", + "SCCACHE_IDLE_TIMEOUT": "${localEnv:SCCACHE_IDLE_TIMEOUT:0}", + "SCCACHE_REGION": "us-east-2", + "SCCACHE_S3_USE_PREPROCESSOR_CACHE_MODE": "true", + "SCCACHE_SERVER_LOG": "${localEnv:SCCACHE_SERVER_LOG:sccache=debug}" + }, + "initializeCommand": [ + "/bin/bash", + "-c", + "mkdir -m 0755 -p ${localWorkspaceFolder}/.{aws,cache,config,local/state} ${localWorkspaceFolder}/ci/rapids/.{conda,log/devcontainer-utils} ${localWorkspaceFolder}/ci/rapids/.repos/{rmm,kvikio,ucxx,cudf,rapidsmpf,raft,cuvs,nvforest,cuml,cugraph,cugraph-gnn,cuopt}" + ], + "postCreateCommand": [ + "/bin/bash", + "-c", + "if [ ${CI:-false} = 'false' ]; then . /home/coder/cccl/ci/rapids/post-create-command.sh; fi; if test -z \"${DISABLE_SCCACHE:+x}\"; then echo \"export SCCACHE_DIST_URL='https://$(uname -m | sed -e 's/x86_/amd/' -e 's/aarch/arm/').linux.sccache.rapids.nvidia.com'\" >> /home/coder/.bashrc; fi" + ], + "postAttachCommand": [ + "/bin/bash", + "-c", + "if [ ${CODESPACES:-false} = 'true' ]; then . devcontainer-utils-post-attach-command; fi" + ], + "workspaceFolder": "/home/coder/cccl", + "workspaceMount": "source=${localWorkspaceFolder},target=/home/coder/cccl,type=bind", + "mounts": [ + "source=/etc/timezone,target=/etc/timezone,type=bind", + "source=/etc/localtime,target=/etc/localtime,type=bind", + "source=${localWorkspaceFolder}/.aws,target=/home/coder/.aws,type=bind", + "source=${localWorkspaceFolder}/.cache,target=/home/coder/.cache,type=bind", + "source=${localWorkspaceFolder}/.config,target=/home/coder/.config,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/rmm,target=/home/coder/rmm,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/kvikio,target=/home/coder/kvikio,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/ucxx,target=/home/coder/ucxx,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cudf,target=/home/coder/cudf,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/rapidsmpf,target=/home/coder/rapidsmpf,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/raft,target=/home/coder/raft,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cuvs,target=/home/coder/cuvs,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/nvforest,target=/home/coder/nvforest,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cuml,target=/home/coder/cuml,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cugraph,target=/home/coder/cugraph,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cugraph-gnn,target=/home/coder/cugraph-gnn,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.repos/cuopt,target=/home/coder/cuopt,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.conda,target=/home/coder/.conda,type=bind", + "source=${localWorkspaceFolder}/ci/rapids/.log/devcontainer-utils,target=/var/log/devcontainer-utils,type=bind" + ], + "features": { + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:26.10": {} + }, + "overrideFeatureInstallOrder": [ + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" + ], + "customizations": { + "vscode": { + "extensions": [ + "augustocdias.tasks-shell-input", + "ms-python.flake8", + "nvidia.nsight-vscode-edition" + ], + "files.watcherExclude": { + "**/build/**": true, + "**/_skbuild/**": true, + "**/target/**": true, + "/home/coder/.aws/**/*": true, + "/home/coder/.cache/**/*": true, + "/home/coder/.conda/**/*": true, + "/home/coder/.local/share/**/*": true, + "/home/coder/.vscode-server/**/*": true + }, + "search.exclude": { + "**/build/**": true, + "**/_skbuild/**": true, + "**/*.code-search": true, + "/home/coder/.aws/**/*": true, + "/home/coder/.cache/**/*": true, + "/home/coder/.conda/**/*": true, + "/home/coder/.local/share/**/*": true, + "/home/coder/.vscode-server/**/*": true + } + } + } +} diff --git a/cccl_upstream/ci/rapids/post-create-command.sh b/cccl_upstream/ci/rapids/post-create-command.sh new file mode 100755 index 00000000..db427da1 --- /dev/null +++ b/cccl_upstream/ci/rapids/post-create-command.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash + +PROJECT_MANIFEST_YML="${PROJECT_MANIFEST_YML:-"/opt/rapids-build-utils/manifest.yaml"}" + +_restore_original_manifest() { + if ! test -f "/tmp/manifest.yaml.orig"; then + cp "${PROJECT_MANIFEST_YML}" /tmp/manifest.yaml.orig; + fi + sudo cp /tmp/manifest.yaml.orig "${PROJECT_MANIFEST_YML}"; +} + +_apply_manifest_modifications() { + # Restore the original manifest.yaml + _restore_original_manifest; + # Remove unnecessary libs from manifest.yaml + _prune_libs_from_manifest; + # Update manifest.yaml repo git info + _update_repo_git_info; + # Create rapids-cmake override JSON file and update default CMake arguments in manifest.yaml + _create_rapids_cmake_override_json; + # Print the entire manifest.yaml after modifications + cat "${PROJECT_MANIFEST_YML}"; + # Regenerate the RAPIDS build scripts from the new manifest.yaml + rapids-generate-scripts; +} + +_prune_libs_from_manifest() { + local -; + set -euo pipefail; + if test -n "${RAPIDS_LIBS-}"; then + local -a filters="(${RAPIDS_LIBS})"; + # prefix each element + filters=("${filters[@]/#/'"'}"); + # suffix each element + filters=("${filters[@]/%/'",'}"); + # Remove trailing comma + local -r filters_str="$(cut -d',' -f1-${#filters[@]} <<< "${filters[*]}")"; + sudo yq -i ".repos |= filter(.cpp[].name | contains(${filters_str}))" "${PROJECT_MANIFEST_YML}"; + fi +} + +# shellcheck disable=SC2016 +_update_repo_git_info() { + local -; + set -euo pipefail; + yq '.repos[].name' "${PROJECT_MANIFEST_YML}" \ + | xargs -r -n1 bash -c ' + var="RAPIDS_${1//-/_}_GIT_REPO"; + if test -v "${var}" && test -n "${!var}"; then + sudo yq -i "(.repos[] | select(.name == \"$1\") | .git) *= ${!var}" "${0}"; + fi' "${PROJECT_MANIFEST_YML}"; +} + +_create_rapids_cmake_override_json() { + local -; + set -euo pipefail; + local rapids_cmake_tag=; + local rapids_cmake_upstream=; + if test -n "${RAPIDS_cmake_GIT_REPO-}"; then + rapids_cmake_tag="$(jq -r '.tag' <<< "${RAPIDS_cmake_GIT_REPO}")"; + rapids_cmake_upstream="$(jq -r '.upstream' <<< "${RAPIDS_cmake_GIT_REPO}")"; + else + rapids_cmake_tag="$(yq '.x-git-defaults.tag' /opt/rapids-build-utils/manifest.yaml)"; + rapids_cmake_upstream="$(yq '.x-git-defaults.upstream' /opt/rapids-build-utils/manifest.yaml)"; + fi + + # Define CCCL_TAG to override the default CCCL SHA. Otherwise the current HEAD of the local checkout is used. + if test -n "${CCCL_TAG-}"; then + # If CCCL_TAG is defined, fetch it to the local checkout + git -C "${HOME}/cccl" fetch origin "${CCCL_TAG}"; + cccl_sha="$(git -C "${HOME}/cccl" rev-parse FETCH_HEAD)"; + else + cccl_sha="$(git -C "${HOME}/cccl" rev-parse HEAD)"; + fi + echo "CCCL_VERSION: ${CCCL_VERSION-}"; + echo "CCCL_TAG: ${CCCL_TAG-}"; + echo "cccl_sha: ${cccl_sha}"; + + echo + echo "Replacing CCCL repo information in rapids-cmake versions.json:"; + curl -fsSL -o- "https://raw.githubusercontent.com/${rapids_cmake_upstream}/rapids-cmake/${rapids_cmake_tag}/rapids-cmake/cpm/versions.json" \ + | jq -r ".packages.CCCL *= {\"git_url\": \"${HOME}/cccl\", \"git_tag\": \"${cccl_sha}\", \"always_download\": true}" \ + | jq -r "del(.packages.CCCL.url) | del(.packages.CCCL.url_hash)" \ + > ~/rapids-cmake-override-versions-cccl-repo.json; + + if test -n "${CCCL_VERSION-}"; then + echo "Patching CCCL_VERSION in rapids-cmake versions.json:"; + jq -r ".packages.CCCL.version = \"${CCCL_VERSION}\"" ~/rapids-cmake-override-versions-cccl-repo.json \ + > ~/rapids-cmake-override-versions.json; + else + echo "Using the default CCCL version in rapids-cmake versions.json:"; + mv ~/rapids-cmake-override-versions-cccl-repo.json ~/rapids-cmake-override-versions.json; + fi + + echo + echo "Final rapids-cmake-override-versions.json:"; + cat ~/rapids-cmake-override-versions.json; + + # Define default CMake args for each repo + local -a cmake_args=(BUILD_TESTS BUILD_BENCHMARKS BUILD_PRIMS_BENCH BUILD_CUGRAPH_MG_TESTS); + # Enable tests + cmake_args=("${cmake_args[@]/#/"-D"}"); + cmake_args=("${cmake_args[@]/%/"=${RAPIDS_ENABLE_TESTS:-ON}"}"); + + # Always build RAFT shared lib + cmake_args+=("-DBUILD_SHARED_LIBS=ON"); + cmake_args+=("-DRAFT_COMPILE_LIBRARY=ON"); + + # Tell rapids-cmake to use custom CCCL and cuCollections forks + cmake_args+=("-Drapids-cmake-branch=${rapids_cmake_tag}"); + cmake_args+=("-Drapids-cmake-repo=${rapids_cmake_upstream}/rapids-cmake"); + cmake_args+=("-DRAPIDS_CMAKE_CPM_DEFAULT_VERSION_FILE=${HOME}/rapids-cmake-override-versions.json"); + + # Inject the CMake args into manifest.yaml + sudo yq -i "(.repos[] | .cpp[] | .args.cmake) += \" ${cmake_args[*]}\"" "${PROJECT_MANIFEST_YML}"; + sudo yq -i "(.repos[] | .python[] | .args.cmake) += \" ${cmake_args[*]}\"" "${PROJECT_MANIFEST_YML}"; +} + +_run_post_create_command() { + local -; + set -e; + + # Install `rapids-build-utils` feature if it's not installed (i.e. if running locally via `.devcontainer/launch.sh -d`) + if ! test -f "${PROJECT_MANIFEST_YML}"; then + git clone --depth 1 --filter=blob:none --sparse https://github.com/rapidsai/devcontainers.git /tmp/rapidsai-devcontainers; + git -C /tmp/rapidsai-devcontainers sparse-checkout set features/src/rapids-build-utils; + ( + cd /tmp/rapidsai-devcontainers/features/src/rapids-build-utils; + sudo bash ./install.sh; + ) + rm -rf /tmp/rapidsai-devcontainers; + fi + + # Modify manifest.yaml based on envvars + _apply_manifest_modifications; + + # Clone all the repos + gh config set git_protocol https; + gh config set git_protocol https --host github.com; + + clone-all -j "$(nproc --all)" -v -q --clone-upstream --single-branch --shallow-submodules --no-update-env; +} + +if [[ "$(basename "${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}")" = post-create-command.sh ]]; then + _run_post_create_command; +fi diff --git a/cccl_upstream/ci/rapids/rapids-entrypoint.sh b/cccl_upstream/ci/rapids/rapids-entrypoint.sh new file mode 100755 index 00000000..b8b43112 --- /dev/null +++ b/cccl_upstream/ci/rapids/rapids-entrypoint.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 + +set -e; + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::group::Cloning RAPIDS..." +fi + +ci/rapids/post-create-command.sh; +rapids-post-start-command -f; + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::endgroup::" +fi + +if test $# -gt 0; then + exec "$@"; +else + exec /bin/bash -li; +fi diff --git a/cccl_upstream/ci/run_cpu_bisect.sh b/cccl_upstream/ci/run_cpu_bisect.sh new file mode 100755 index 00000000..3bc7dd3c --- /dev/null +++ b/cccl_upstream/ci/run_cpu_bisect.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# CI wrapper for the `bisect` project build job. +# Forwards all arguments to ci/util/git_bisect.sh to run a bisection +# using the provided configuration/build/test options. + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_dir=$(cd "${ci_dir}/.." && pwd) + +user_args=("$@") +set -- +source "${ci_dir}/build_common.sh" +set -- "${user_args[@]}" + +cd "${repo_dir}" +cmd=("${ci_dir}/util/git_bisect.sh" "$@") +printf '\033[34m%s\033[0m\n' "${cmd[*]}" +"${cmd[@]}" diff --git a/cccl_upstream/ci/run_cpu_target.sh b/cccl_upstream/ci/run_cpu_target.sh new file mode 100755 index 00000000..81951887 --- /dev/null +++ b/cccl_upstream/ci/run_cpu_target.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# CI wrapper for the `target` project build job. +# Forwards all arguments to ci/util/build_and_test_targets.sh to configure +# and build selected targets on a CPU runner. + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_dir=$(cd "${ci_dir}/.." && pwd) + +user_args=("$@") +set -- +source "${ci_dir}/build_common.sh" +set -- "${user_args[@]}" + +cd "${repo_dir}" +cmd=("${ci_dir}/util/build_and_test_targets.sh" "$@") +printf '\033[34m%s\033[0m\n' "${cmd[*]}" +"${cmd[@]}" diff --git a/cccl_upstream/ci/run_gpu_bisect.sh b/cccl_upstream/ci/run_gpu_bisect.sh new file mode 100755 index 00000000..41a7ddc2 --- /dev/null +++ b/cccl_upstream/ci/run_gpu_bisect.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# CI wrapper for the `bisect` project test job. +# Invokes ci/util/git_bisect.sh with the provided arguments to +# run a bisection on a GPU runner. + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_dir=$(cd "${ci_dir}/.." && pwd) + +user_args=("$@") +set -- +source "${ci_dir}/build_common.sh" +set -- "${user_args[@]}" + +cd "${repo_dir}" +cmd=("${ci_dir}/util/git_bisect.sh" "$@") +printf '\033[34m%s\033[0m\n' "${cmd[*]}" +"${cmd[@]}" diff --git a/cccl_upstream/ci/run_gpu_target.sh b/cccl_upstream/ci/run_gpu_target.sh new file mode 100755 index 00000000..4bc25bfa --- /dev/null +++ b/cccl_upstream/ci/run_gpu_target.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# CI wrapper for the `target` project test job. +# Invokes ci/util/build_and_test_targets.sh with the provided arguments to +# build and test selected targets on a GPU runner. + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_dir=$(cd "${ci_dir}/.." && pwd) + +user_args=("$@") +set -- +source "${ci_dir}/build_common.sh" +set -- "${user_args[@]}" + +cd "${repo_dir}" +cmd=("${ci_dir}/util/build_and_test_targets.sh" "$@") +printf '\033[34m%s\033[0m\n' "${cmd[*]}" +"${cmd[@]}" diff --git a/cccl_upstream/ci/test/CMakeLists.txt b/cccl_upstream/ci/test/CMakeLists.txt new file mode 100644 index 00000000..e801d6e1 --- /dev/null +++ b/cccl_upstream/ci/test/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory("inspect_changes") diff --git a/cccl_upstream/ci/test/inspect_changes/CMakeLists.txt b/cccl_upstream/ci/test/inspect_changes/CMakeLists.txt new file mode 100644 index 00000000..fda2c0f4 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/CMakeLists.txt @@ -0,0 +1,22 @@ +find_package(Python3 REQUIRED COMPONENTS Interpreter) + +file(GLOB output_files "${CMAKE_CURRENT_SOURCE_DIR}/*.output") +foreach (output_file IN LISTS output_files) + get_filename_component(test_name "${output_file}" NAME_WE) + set(dirty_file "${CMAKE_CURRENT_SOURCE_DIR}/${test_name}.dirty_files") + if (NOT EXISTS "${dirty_file}") + message(FATAL_ERROR "Missing dirty file for ${test_name}") + endif() + + add_test( + NAME cccl.ci.test.inspect_changes.${test_name} + # gersemi: off + COMMAND + "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/run_inspect_changes_test.py" + "--python" "${Python3_EXECUTABLE}" + "--script" "${PROJECT_SOURCE_DIR}/ci/inspect_changes.py" + "--dirty" "${dirty_file}" + "--expected" "${output_file}" + # gersemi: on + ) +endforeach() diff --git a/cccl_upstream/ci/test/inspect_changes/c2h_dependency.dirty_files b/cccl_upstream/ci/test/inspect_changes/c2h_dependency.dirty_files new file mode 100644 index 00000000..8938dfc5 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/c2h_dependency.dirty_files @@ -0,0 +1 @@ +c2h/catch2_runner.cu diff --git a/cccl_upstream/ci/test/inspect_changes/c2h_dependency.output b/cccl_upstream/ci/test/inspect_changes/c2h_dependency.output new file mode 100644 index 00000000..92df1f7a --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/c2h_dependency.output @@ -0,0 +1,2 @@ +FULL_BUILD=tidy +LITE_BUILD=libcudacxx cub cudax cccl_c_parallel cccl_c_parallel_v2 python_v2 cccl_c_stf packaging diff --git a/cccl_upstream/ci/test/inspect_changes/core_dirty.dirty_files b/cccl_upstream/ci/test/inspect_changes/core_dirty.dirty_files new file mode 100644 index 00000000..5e71f5bf --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/core_dirty.dirty_files @@ -0,0 +1 @@ +CMakePresets.json diff --git a/cccl_upstream/ci/test/inspect_changes/core_dirty.output b/cccl_upstream/ci/test/inspect_changes/core_dirty.output new file mode 100644 index 00000000..456e3158 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/core_dirty.output @@ -0,0 +1,2 @@ +FULL_BUILD=libcudacxx cub thrust cudax cccl_c_parallel cccl_c_parallel_v2 python_v2 python_tsan cccl_c_stf python packaging stdpar nvbench_helper nvrtcc tidy +LITE_BUILD= diff --git a/cccl_upstream/ci/test/inspect_changes/ignored_only.dirty_files b/cccl_upstream/ci/test/inspect_changes/ignored_only.dirty_files new file mode 100644 index 00000000..7d2fa473 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/ignored_only.dirty_files @@ -0,0 +1 @@ +docs/index.rst diff --git a/cccl_upstream/ci/test/inspect_changes/ignored_only.output b/cccl_upstream/ci/test/inspect_changes/ignored_only.output new file mode 100644 index 00000000..4e9e2ba7 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/ignored_only.output @@ -0,0 +1,2 @@ +FULL_BUILD= +LITE_BUILD= diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.dirty_files b/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.dirty_files new file mode 100644 index 00000000..a96b1b4e --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.dirty_files @@ -0,0 +1,2 @@ +libcudacxx/CMakeLists.txt +libcudacxx/include/cuda/__device/device_ref.h diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.output b/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.output new file mode 100644 index 00000000..eddc7707 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_both.output @@ -0,0 +1,2 @@ +FULL_BUILD=libcudacxx tidy +LITE_BUILD=cub thrust cudax cccl_c_parallel cccl_c_parallel_v2 python_v2 python_tsan cccl_c_stf python packaging stdpar nvbench_helper diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.dirty_files b/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.dirty_files new file mode 100644 index 00000000..879d2d5f --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.dirty_files @@ -0,0 +1 @@ +libcudacxx/CMakeLists.txt diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.output b/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.output new file mode 100644 index 00000000..8e8a2194 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_internal_only.output @@ -0,0 +1,2 @@ +FULL_BUILD=libcudacxx tidy +LITE_BUILD=packaging diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.dirty_files b/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.dirty_files new file mode 100644 index 00000000..421343ca --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.dirty_files @@ -0,0 +1 @@ +libcudacxx/include/cuda/__device/device_ref.h diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.output b/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.output new file mode 100644 index 00000000..eddc7707 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_public_only.output @@ -0,0 +1,2 @@ +FULL_BUILD=libcudacxx tidy +LITE_BUILD=cub thrust cudax cccl_c_parallel cccl_c_parallel_v2 python_v2 python_tsan cccl_c_stf python packaging stdpar nvbench_helper diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.dirty_files b/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.dirty_files new file mode 100644 index 00000000..0e8b7d4e --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.dirty_files @@ -0,0 +1,3 @@ +libcudacxx/include/cuda/__device/device_ref.h +thrust/thrust/version.h +README.md diff --git a/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.output b/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.output new file mode 100644 index 00000000..00f10a62 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/libcudacxx_thrust.output @@ -0,0 +1,2 @@ +FULL_BUILD=libcudacxx thrust tidy +LITE_BUILD=cub cudax cccl_c_parallel cccl_c_parallel_v2 python_v2 python_tsan cccl_c_stf python packaging stdpar nvbench_helper diff --git a/cccl_upstream/ci/test/inspect_changes/multiple_projects.dirty_files b/cccl_upstream/ci/test/inspect_changes/multiple_projects.dirty_files new file mode 100644 index 00000000..a91f6065 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/multiple_projects.dirty_files @@ -0,0 +1,2 @@ +python/cuda_cccl/pyproject.toml +examples/basic/CMakeLists.txt diff --git a/cccl_upstream/ci/test/inspect_changes/multiple_projects.output b/cccl_upstream/ci/test/inspect_changes/multiple_projects.output new file mode 100644 index 00000000..9395cf0a --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/multiple_projects.output @@ -0,0 +1,2 @@ +FULL_BUILD=python_v2 python_tsan python packaging +LITE_BUILD= diff --git a/cccl_upstream/ci/test/inspect_changes/no_changes.dirty_files b/cccl_upstream/ci/test/inspect_changes/no_changes.dirty_files new file mode 100644 index 00000000..e69de29b diff --git a/cccl_upstream/ci/test/inspect_changes/no_changes.output b/cccl_upstream/ci/test/inspect_changes/no_changes.output new file mode 100644 index 00000000..4e9e2ba7 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/no_changes.output @@ -0,0 +1,2 @@ +FULL_BUILD= +LITE_BUILD= diff --git a/cccl_upstream/ci/test/inspect_changes/packaging_only.dirty_files b/cccl_upstream/ci/test/inspect_changes/packaging_only.dirty_files new file mode 100644 index 00000000..f1ec2e00 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/packaging_only.dirty_files @@ -0,0 +1 @@ +examples/CMakeLists.txt diff --git a/cccl_upstream/ci/test/inspect_changes/packaging_only.output b/cccl_upstream/ci/test/inspect_changes/packaging_only.output new file mode 100644 index 00000000..a23e2374 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/packaging_only.output @@ -0,0 +1,2 @@ +FULL_BUILD=packaging +LITE_BUILD= diff --git a/cccl_upstream/ci/test/inspect_changes/regenerate_outputs.sh b/cccl_upstream/ci/test/inspect_changes/regenerate_outputs.sh new file mode 100755 index 00000000..464d09ba --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/regenerate_outputs.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +project_root=$(cd "${script_dir}/../../.." && pwd) +inspect_changes="${project_root}/ci/inspect_changes.py" + +if [[ ! -x "${inspect_changes}" ]]; then + echo "Error: ${inspect_changes} not found or not executable." >&2 + exit 1 +fi + +cd "${script_dir}" + +for dirty_file in *.dirty_files; do + test_name=${dirty_file%.dirty_files} + output_file="${test_name}.output" + echo "Regenerating ${output_file}" + python "${inspect_changes}" --file "${dirty_file}" \ + | awk '/^FULL_BUILD=/{print}/^LITE_BUILD=/{print}' > "${output_file}" +done diff --git a/cccl_upstream/ci/test/inspect_changes/run_inspect_changes_test.py b/cccl_upstream/ci/test/inspect_changes/run_inspect_changes_test.py new file mode 100755 index 00000000..94dccd57 --- /dev/null +++ b/cccl_upstream/ci/test/inspect_changes/run_inspect_changes_test.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Test harness for ci/inspect_changes.py.""" + +from __future__ import annotations + +import argparse +import difflib +import subprocess +import sys +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run inspect_changes.py and compare output" + ) + parser.add_argument( + "--script", required=True, type=Path, help="Path to inspect_changes.py" + ) + parser.add_argument( + "--dirty", required=True, type=Path, help="File listing dirty files" + ) + parser.add_argument( + "--expected", required=True, type=Path, help="Expected stdout contents" + ) + parser.add_argument( + "--python", default=sys.executable, help="Python interpreter to use" + ) + return parser.parse_args() + + +def build_command(python: str, script: Path, dirty_file: Path) -> list[str]: + cmd = [python, str(script), "--file", str(dirty_file)] + return cmd + + +def main() -> int: + args = parse_args() + cmd = build_command(args.python, args.script, args.dirty) + + sys.stdout.write(f"COMMAND: {' '.join(cmd)}\n") + result = subprocess.run(cmd, capture_output=True, text=True) + sys.stdout.write("OUTPUT:\n") + sys.stdout.write(result.stdout) + if result.stdout and not result.stdout.endswith("\n"): + sys.stdout.write("\n") + if result.returncode != 0: + sys.stderr.write(result.stderr) + sys.stderr.write("\nCommand failed: {}\n".format(" ".join(cmd))) + return result.returncode + + actual_lines = result.stdout.splitlines() + expected_lines = [ + line.strip() + for line in args.expected.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + missing = [line for line in expected_lines if line not in actual_lines] + if missing: + diff = "\n".join( + difflib.unified_diff( + expected_lines, + actual_lines, + fromfile="expected(subset)", + tofile="actual", + lineterm="", + ) + ) + if diff: + sys.stderr.write(diff + "\n\n") + sys.stderr.write("\nExpected lines missing from output:\n") + for line in missing: + sys.stderr.write(f" {line}\n") + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cccl_upstream/ci/test_cccl_c_parallel.sh b/cccl_upstream/ci/test_cccl_c_parallel.sh new file mode 100755 index 00000000..01fe14af --- /dev/null +++ b/cccl_upstream/ci/test_cccl_c_parallel.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +./build_cccl_c_parallel.sh "$@" + +PRESET="cccl-c-parallel" + +test_preset "CCCL C Parallel Library" "${PRESET}" + +print_time_summary diff --git a/cccl_upstream/ci/test_cccl_c_parallel_v2.sh b/cccl_upstream/ci/test_cccl_c_parallel_v2.sh new file mode 100755 index 00000000..02c893f4 --- /dev/null +++ b/cccl_upstream/ci/test_cccl_c_parallel_v2.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +# libnvfatbin is required by hostjit but is not included in the base rapidsai devcontainer +# image. Detect the installed CTK version and install the matching package if missing. +if [[ "$(uname -s)" == "Linux" ]] && ! ldconfig -p 2>/dev/null | grep -q libnvfatbin; then + CTK_DEB_VER=$(nvcc --version 2>/dev/null \ + | grep -oP 'release \K[0-9]+\.[0-9]+' | tr '.' '-') + if [[ -n "$CTK_DEB_VER" ]]; then + echo "Installing libnvfatbin-dev-${CTK_DEB_VER}..." + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends "libnvfatbin-dev-${CTK_DEB_VER}" + else + echo "WARNING: could not determine CTK version; skipping libnvfatbin install" + fi +fi + +PRESET="cccl-c-parallel-v2" + +CMAKE_OPTIONS=() +if test -n "${CXX_STANDARD:+x}"; then + CMAKE_OPTIONS+=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") +fi + +configure_and_build_preset "CCCL C Parallel Library v2 (HostJIT)" "$PRESET" "${CMAKE_OPTIONS[@]}" + +test_preset "CCCL C Parallel Library v2 (HostJIT)" "$PRESET" + +print_time_summary diff --git a/cccl_upstream/ci/test_cccl_c_stf.sh b/cccl_upstream/ci/test_cccl_c_stf.sh new file mode 100755 index 00000000..410e7ae4 --- /dev/null +++ b/cccl_upstream/ci/test_cccl_c_stf.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +./build_cccl_c_stf.sh "$@" + +PRESET="cccl-c-stf" + +test_preset "CCCL C Parallel Library" "${PRESET}" + +print_time_summary diff --git a/cccl_upstream/ci/test_cub.sh b/cccl_upstream/ci/test_cub.sh new file mode 100755 index 00000000..d111be86 --- /dev/null +++ b/cccl_upstream/ci/test_cub.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash + +set -euo pipefail + +NO_LID=false +LID0=false +LID1=false +LID2=false +LIMITED=false +COMPUTE_SANITIZER=false +ARTIFACT_TAGS=() + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +new_args="$("${ci_dir}/util/extract_switches.sh" \ + -no-lid \ + -lid0 \ + -lid1 \ + -lid2 \ + -limited \ + -compute-sanitizer-memcheck \ + -compute-sanitizer-racecheck \ + -compute-sanitizer-initcheck \ + -compute-sanitizer-synccheck \ + -- "$@")" + +declare -a new_args="(${new_args})" +set -- "${new_args[@]}" +while true; do + case "$1" in + -no-lid) + ARTIFACT_TAGS+=("no_lid") + NO_LID=true + shift + ;; + -lid0) + ARTIFACT_TAGS+=("lid_0") + LID0=true + shift + ;; + -lid1) + ARTIFACT_TAGS+=("lid_1") + LID1=true + shift + ;; + -lid2) + ARTIFACT_TAGS+=("lid_2") + LID2=true + shift + ;; + -limited) + # Pull all artifacts: + ARTIFACT_TAGS+=("no_lid" "lid_0" "lid_1" "lid_2") + LIMITED=true + shift + ;; + -compute-sanitizer-memcheck) + COMPUTE_SANITIZER=true + TOOL=memcheck + shift + ;; + -compute-sanitizer-racecheck) + COMPUTE_SANITIZER=true + TOOL=racecheck + shift + ;; + -compute-sanitizer-initcheck) + COMPUTE_SANITIZER=true + TOOL=initcheck + shift + ;; + -compute-sanitizer-synccheck) + COMPUTE_SANITIZER=true + TOOL=synccheck + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + +if $LIMITED; then + + export C2H_SEED_COUNT_OVERRIDE=1 + readonly device_mem_GiB=8 + export C2H_DEVICE_MEMORY_LIMIT=$((device_mem_GiB * 1024 * 1024 * 1024)) + export C2H_DEBUG_CHECKED_ALLOC_FAILURES=1 + + echo "Configuring limited environment:" + echo " C2H_SEED_COUNT_OVERRIDE=${C2H_SEED_COUNT_OVERRIDE}" + echo " C2H_DEVICE_MEMORY_LIMIT=${C2H_DEVICE_MEMORY_LIMIT} (${device_mem_GiB} GiB)" + echo " C2H_DEBUG_CHECKED_ALLOC_FAILURES=${C2H_DEBUG_CHECKED_ALLOC_FAILURES}" + echo +fi + +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +print_environment_details + + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + ./build_cub.sh "$@" +else + producer_id=$(util/workflow/get_producer_id.sh) + for tag in "${ARTIFACT_TAGS[@]}"; do + artifact="z_cub-test-artifacts-${DEVCONTAINER_NAME:?}-$producer_id-$tag" + run_command "📦 Unpacking artifact '$artifact'" \ + "${ci_dir}/util/artifacts/download_packed.sh" "$artifact" /home/coder/cccl + done +fi + +if $NO_LID; then + PRESETS=("cub-nolid") +elif $LID0; then + PRESETS=("cub-lid0") +elif $LID1; then + PRESETS=("cub-lid1") +elif $LID2; then + PRESETS=("cub-lid2") +else + PRESETS=("cub") +fi + +if $COMPUTE_SANITIZER; then + echo "Setting CCCL_TEST_MODE=compute-sanitizer-${TOOL}" + export CCCL_TEST_MODE=compute-sanitizer-${TOOL} + echo "Setting C2H_SEED_COUNT_OVERRIDE=1" + export C2H_SEED_COUNT_OVERRIDE=1 +fi + +for PRESET in "${PRESETS[@]}"; do + test_preset "CUB (${PRESET})" "${PRESET}" +done + +print_time_summary diff --git a/cccl_upstream/ci/test_cuda_cccl_examples_python.sh b/cccl_upstream/ci/test_cuda_cccl_examples_python.sh new file mode 100755 index 00000000..5a3e17e5 --- /dev/null +++ b/cccl_upstream/ci/test_cuda_cccl_examples_python.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$ci_dir/pyenv_helper.sh" + +# Parse common arguments +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" +# Pin cuda-toolkit to the container's CTK minor and set cuda_version / +# cuda_major_version (-ctk-mode latest opts out). See pyenv_helper.sh. +pin_cuda_toolkit "${ctk_mode}" + +# Setup Python environment +setup_python_env "${py_version}" + +# Fetch or build the cuda_cccl wheel: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name=$("$ci_dir/util/workflow/get_wheel_artifact_name.sh") + "$ci_dir/util/artifacts/download.sh" "${wheel_artifact_name}" /home/coder/cccl/ +else + "$ci_dir/build_cuda_cccl_python.sh" -py-version "${py_version}" +fi + +# Install cuda_cccl, plus CuPy which the cuda.compute examples require, plus +# pytest-benchmark for the host-overhead benchmark smoke test below. (cuda-bench, +# for the throughput smoke, is installed best-effort further down since it does +# not always ship a wheel for the newest Python.) +CUDA_CCCL_WHEEL_PATH="$(ls /home/coder/cccl/wheelhouse/cuda_cccl-*.whl)" +ctk_flavor="$(ctk_extra_flavor "${ctk_mode}")" +python -m pip install "${CUDA_CCCL_WHEEL_PATH}[test-${ctk_flavor}${cuda_major_version}]" "cupy-cuda${cuda_major_version}x" pytest-benchmark + +# Run tests for parallel module +cd "/home/coder/cccl/python/cuda_cccl/tests/" +python -m pytest -n 6 test_examples.py + +# Smoke-test the host-overhead benchmark harness: run every benchmark case +# exactly once (pass/fail only, no timing) so harness rot fails CI here instead +# of silently surviving until someone runs the perf suite. +cd "/home/coder/cccl/python/cuda_cccl/benchmarks/compute/host/" +python -m pytest -v --benchmark-disable . + +# Smoke-test the throughput (nvbench) benchmarks the same way. --profile runs +# each configuration once (no sampling); --quick uses the reduced quick_configs +# axes (one dtype, smallest size) so every benchmark harness still imports, +# registers, launches, and completes. cuda-bench does not always ship a wheel for +# the newest Python, so skip the throughput smoke ONLY for that known no-wheel +# case. Note pip prints "No matching distribution"/"Could not find a version" +# even when the index is unreachable, so check for fetch/network failures first +# and fail on those; any other install error fails the lane too rather than +# silently passing. tee streams pip's output live while capturing it for the +# grep checks below (pipefail keeps pip's exit status, not tee's). +install_log="$(mktemp)" +if python -m pip install "cuda-bench[cu${cuda_major_version}]" pyyaml 2>&1 | tee "${install_log}"; then + cd "/home/coder/cccl/python/cuda_cccl/benchmarks/compute/" + python run_benchmarks.py --py --profile --quick +elif grep -qiE "Could not fetch URL|Retrying \(Retry|connection broken|Failed to establish a new connection|Name or service not known|timed out|SSLError|certificate verify failed|ProxyError" "${install_log}"; then + echo "::error::cuda-bench install failed because pip could not reach the package index (network/DNS/TLS/auth); not skipping." >&2 + exit 1 +elif grep -qiE "No matching distribution found for cuda-bench|Could not find a version that satisfies the requirement cuda-bench" "${install_log}"; then + echo "::warning::cuda-bench has no wheel for Python ${py_version}; skipping the throughput benchmark smoke test." +else + echo "::error::cuda-bench install failed for an unrecognized reason." >&2 + exit 1 +fi +rm -f "${install_log}" diff --git a/cccl_upstream/ci/test_cuda_cccl_examples_python_v2.sh b/cccl_upstream/ci/test_cuda_cccl_examples_python_v2.sh new file mode 100755 index 00000000..ae852085 --- /dev/null +++ b/cccl_upstream/ci/test_cuda_cccl_examples_python_v2.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Run the cuda.compute examples against a wheel built with the v2 (HostJIT) +# backend. Mirrors test_cuda_cccl_examples_python.sh; the only difference is +# exporting CCCL_PYTHON_USE_V2 so the wheel build uses cccl.c.parallel.v2. +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CCCL_PYTHON_USE_V2=1 +exec "$ci_dir/test_cuda_cccl_examples_python.sh" "$@" diff --git a/cccl_upstream/ci/test_cuda_cccl_headers_python.sh b/cccl_upstream/ci/test_cuda_cccl_headers_python.sh new file mode 100755 index 00000000..ee39630d --- /dev/null +++ b/cccl_upstream/ci/test_cuda_cccl_headers_python.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -euo pipefail +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "$ci_dir/pyenv_helper.sh" + +# Parse common arguments +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" +# Pin cuda-toolkit to the container's CTK minor and set cuda_version / +# cuda_major_version (-ctk-mode latest opts out). See pyenv_helper.sh. +pin_cuda_toolkit "${ctk_mode}" + +# Setup Python environment +setup_python_env "${py_version}" + +# Fetch or build the cuda_cccl wheel: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name=$("$ci_dir/util/workflow/get_wheel_artifact_name.sh") + "$ci_dir/util/artifacts/download.sh" "${wheel_artifact_name}" /home/coder/cccl/ +else + "$ci_dir/build_cuda_cccl_python.sh" -py-version "${py_version}" +fi + +# Install cuda_cccl +CUDA_CCCL_WHEEL_PATH="$(ls /home/coder/cccl/wheelhouse/cuda_cccl-*.whl)" +ctk_flavor="$(ctk_extra_flavor "${ctk_mode}")" +python -m pip install "${CUDA_CCCL_WHEEL_PATH}[test-${ctk_flavor}${cuda_major_version}]" + +# Run tests for core package +cd "/home/coder/cccl/python/cuda_cccl/tests/" +python -m pytest -n auto -v headers/ diff --git a/cccl_upstream/ci/test_cuda_compute_minimal_python.sh b/cccl_upstream/ci/test_cuda_compute_minimal_python.sh new file mode 100755 index 00000000..fc2635d3 --- /dev/null +++ b/cccl_upstream/ci/test_cuda_compute_minimal_python.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$ci_dir/.." && pwd)" +source "$ci_dir/pyenv_helper.sh" + +# Parse common arguments +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" +require_py_version "Usage: $0 -py-version " + +# Pin cuda-toolkit to the container's CTK minor and set cuda_version / +# cuda_major_version (-ctk-mode latest opts out). See pyenv_helper.sh. +pin_cuda_toolkit "${ctk_mode}" + +# Setup Python environment +setup_python_env "${py_version}" + +# Fetch or build the cuda_cccl wheel: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name=$("$ci_dir/util/workflow/get_wheel_artifact_name.sh") + "$ci_dir/util/artifacts/download.sh" "${wheel_artifact_name}" "${repo_root}/" + wheelhouse_dir="${repo_root}/wheelhouse" +else + "$ci_dir/build_cuda_cccl_python.sh" -py-version "${py_version}" + wheelhouse_dir="${repo_root}/wheelhouse" +fi + +# Install cuda_cccl with the minimal CUDA extra. This intentionally avoids the +# full cu*/sysctk* extras because those pull in numba/numba-cuda. The flavor is +# "cu" (pip toolkit) or "sysctk" (system toolkit) per the -ctk-mode arg. +CUDA_CCCL_WHEEL_PATH="$(ls "${wheelhouse_dir}"/cuda_cccl-*.whl)" +ctk_flavor="$(ctk_extra_flavor "${ctk_mode}")" +python -m pip install "${CUDA_CCCL_WHEEL_PATH}[minimal-${ctk_flavor}${cuda_major_version}]" +python -m pip install pytest pytest-xdist + +cd "${repo_root}/python/cuda_cccl/tests/" +python -m pytest -n 6 -v compute/test_no_numba.py +if [[ "${py_version}" == "3.14t" ]]; then + # Select only tests that support the minimal extra so pytest does not collect + # tests that import numba-cuda and re-enable the GIL. These tests provide their + # own worker threads, so keep pytest itself in a single process. + # The serialization node-ids are module-skipped on the v2 backend today and + # will start running there automatically once v2 gains serialization support. + python -m pytest -n 0 -v \ + compute/test_free_threading_stress.py \ + compute/test_multi_cc_serialization.py::test_aot_build_result_load_failure_is_shared_and_retryable \ + compute/test_multi_cc_serialization.py::test_aot_serialization_waits_for_canonical_first_load + + # Broad thread-safety sweep (pytest-run-parallel): re-run the numba-free + # functional suite with each test executed concurrently across threads + # (barrier-synchronized start), stressing the process-wide build cache, + # single-flight coordination, and the Cython bindings from many threads at + # once. Complements test_free_threading_stress.py above, which targets specific + # shared-object scenarios by hand. -n 0 so the threads share one interpreter. + # + # --parallel-threads=2 matches CuPy's free-threading CI (the closest GPU + # precedent); a small fixed count bounds GPU-memory pressure from concurrent + # kernels and stays reproducible across runners, unlike =auto (the runner's + # logical-core count). + # + # pytest-run-parallel is only used by this sweep, so install it on the 3.14t + # path rather than for every minimal (e.g. non-free-threaded 3.14) run. + python -m pip install pytest-run-parallel + + # Fail fast if the interpreter is not actually GIL-free (wrong build / + # PYTHON_GIL=1): pytest-run-parallel does NOT catch a GIL that is enabled from + # the start -- it would run threads GIL-serialized and pass vacuously. (A GIL + # *re-enabled mid-run* by a non-free-threaded import IS caught by the plugin, + # which is why we do not pass --ignore-gil-enabled.) + python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is enabled; parallel sweep has no signal'" + python -m pytest -n 0 -v --parallel-threads=2 compute/test_no_numba.py +fi diff --git a/cccl_upstream/ci/test_cuda_compute_minimal_python_tsan.sh b/cccl_upstream/ci/test_cuda_compute_minimal_python_tsan.sh new file mode 100755 index 00000000..26fc69ff --- /dev/null +++ b/cccl_upstream/ci/test_cuda_compute_minimal_python_tsan.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# ThreadSanitizer variant of the minimal free-threaded cuda.compute lane. +# +# Installs the TSan-instrumented cuda_cccl wheel (produced by the `python_tsan` +# build, which compiles c.parallel v1 host code with -fsanitize=thread) and runs +# the free-threaded stress tests + the pytest-run-parallel sweep under the TSan +# runtime. A real data race in c.parallel (e.g. build-owned state mutated at +# launch and shared across threads) fails the nightly here instead of silently +# corrupting results under some interleaving. +# +# Only meaningful on a free-threaded (3.14t) interpreter; the GIL would serialize +# the threads and hide the very races we are looking for. + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$ci_dir/.." && pwd)" +source "$ci_dir/pyenv_helper.sh" + +# Parse common arguments +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" +require_py_version "Usage: $0 -py-version " + +if [[ "${py_version}" != *t ]]; then + echo "ERROR: the TSan lane requires a free-threaded (…t) interpreter; got '${py_version}'." >&2 + echo "On a GIL interpreter the sweep serializes and TSan has no signal." >&2 + exit 1 +fi + +# Instrument c.parallel when this script builds the wheel itself (local runs). In +# CI the wheel is the pre-built `python_tsan` artifact, already instrumented; the +# export is harmless there. +export CCCL_C_PARALLEL_SANITIZE_THREAD=1 + +cuda_major_version=$(nvcc --version | grep release | awk '{print $6}' | tr -d ',' | cut -d '.' -f 1 | cut -d 'V' -f 2) + +# Setup Python environment +setup_python_env "${py_version}" + +# Fetch or build the TSan-instrumented cuda_cccl wheel. Under project +# `python_tsan`, get_wheel_artifact_name.sh resolves to the distinct `-tsan` +# artifact, so this never grabs an uninstrumented wheel. +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name=$("$ci_dir/util/workflow/get_wheel_artifact_name.sh") + "$ci_dir/util/artifacts/download.sh" "${wheel_artifact_name}" "${repo_root}/" + wheelhouse_dir="${repo_root}/wheelhouse" +else + "$ci_dir/build_cuda_cccl_python.sh" -py-version "${py_version}" + wheelhouse_dir="${repo_root}/wheelhouse" +fi + +# minimal-cu* extra intentionally avoids numba/numba-cuda (which re-enable the +# GIL). pytest-run-parallel drives the concurrent sweep. +CUDA_CCCL_WHEEL_PATH="$(ls "${wheelhouse_dir}"/cuda_cccl-*.whl)" +python -m pip install "${CUDA_CCCL_WHEEL_PATH}[minimal-cu${cuda_major_version}]" +python -m pip install pytest pytest-xdist pytest-run-parallel + +# The instrumented .so links libtsan but keeps it external (auditwheel --exclude), +# so the TSan runtime must be present from process start -- LD_PRELOAD the +# runner's libtsan (same soname/major as the gcc-13 build). Without preload the +# .so fails to load ("cannot allocate memory in static TLS block"). +tsan_runtime="$(gcc -print-file-name=libtsan.so.2)" +if [[ ! -e "${tsan_runtime}" ]]; then + echo "ERROR: libtsan.so.2 not found (gcc -print-file-name returned '${tsan_runtime}')." >&2 + exit 1 +fi + +# setarch -R disables ASLR for the process tree. Required: TSan reserves fixed +# shadow-memory regions and aborts ("unexpected memory mapping") when ASLR drops +# something into them (google/sanitizers#1686). Uses personality(ADDR_NO_RANDOMIZE) +# -- if a runner's seccomp profile blocks it, this call fails and must be allowed +# (e.g. --security-opt seccomp=unconfined on the job's container). +# +# ignore_noninstrumented_modules=1: only c.parallel is instrumented, so ignore +# races inside uninstrumented CPython / CUDA libs (avoids boundary false +# positives). halt_on_error=1: stop at the first race -- it is usually the root +# cause, and later reports are typically downstream noise. +# exitcode=66: exit non-zero when any (unsuppressed) race is found, failing the +# job even though pytest itself passes. +run_under_tsan() { + setarch -R env \ + LD_PRELOAD="${tsan_runtime}" \ + TSAN_OPTIONS="ignore_noninstrumented_modules=1 halt_on_error=1 history_size=7 exitcode=66" \ + "$@" +} + +# Fail fast if the interpreter is not actually GIL-free (wrong build / +# PYTHON_GIL=1): the sweep would run GIL-serialized and pass vacuously. +run_under_tsan python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is enabled; TSan sweep has no signal'" + +cd "${repo_root}/python/cuda_cccl/tests/" + +# Hand-written free-threaded stress scenarios (they spawn their own worker +# threads) + the serialization node-ids, all sharing one interpreter (-n 0). +run_under_tsan python -m pytest -n 0 -v \ + compute/test_free_threading_stress.py \ + compute/test_multi_cc_serialization.py::test_aot_build_result_load_failure_is_shared_and_retryable \ + compute/test_multi_cc_serialization.py::test_aot_serialization_waits_for_canonical_first_load + +# Broad sweep: run the numba-free functional suite with each test executed +# concurrently across threads (barrier-synchronized), exercising many more +# c.parallel algorithms under contention than the hand-written stress tests. +# -n 0 so the threads share one interpreter; --parallel-threads=2 bounds +# GPU-memory pressure and stays reproducible across runners. +run_under_tsan python -m pytest -n 0 -v --parallel-threads=2 compute/test_no_numba.py diff --git a/cccl_upstream/ci/test_cuda_compute_minimal_python_v2.sh b/cccl_upstream/ci/test_cuda_compute_minimal_python_v2.sh new file mode 100755 index 00000000..a34c3e3e --- /dev/null +++ b/cccl_upstream/ci/test_cuda_compute_minimal_python_v2.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Run the minimal cuda.compute suite against a wheel built with the v2 +# (HostJIT) backend. The shared minimal script owns dependency installation and +# selects the free-threading stress and serialization tests for Python 3.14t. +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CCCL_PYTHON_USE_V2=1 +exec "$ci_dir/test_cuda_compute_minimal_python.sh" "$@" diff --git a/cccl_upstream/ci/test_cuda_compute_python.sh b/cccl_upstream/ci/test_cuda_compute_python.sh new file mode 100755 index 00000000..01202944 --- /dev/null +++ b/cccl_upstream/ci/test_cuda_compute_python.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$ci_dir/pyenv_helper.sh" + +# Parse common arguments +source "$ci_dir/util/python/common_arg_parser.sh" +parse_python_args "$@" +# Pin cuda-toolkit to the container's CTK minor and set cuda_version / +# cuda_major_version (-ctk-mode latest opts out). See pyenv_helper.sh. +pin_cuda_toolkit "${ctk_mode}" + +# Setup Python environment +setup_python_env "${py_version}" + +# Fetch or build the cuda_cccl wheel: +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + wheel_artifact_name=$("$ci_dir/util/workflow/get_wheel_artifact_name.sh") + "$ci_dir/util/artifacts/download.sh" "${wheel_artifact_name}" /home/coder/cccl/ +else + "$ci_dir/build_cuda_cccl_python.sh" -py-version "${py_version}" +fi + +# Install cuda_cccl. The extra flavor is "cu" (pip-installed toolkit) or "sysctk" +# (system-provided toolkit) depending on the -ctk-mode arg. +CUDA_CCCL_WHEEL_PATH="$(ls /home/coder/cccl/wheelhouse/cuda_cccl-*.whl)" +ctk_flavor="$(ctk_extra_flavor "${ctk_mode}")" +python -m pip install "${CUDA_CCCL_WHEEL_PATH}[test-${ctk_flavor}${cuda_major_version}]" + +# Run tests for compute module. +# On the v2 (HostJIT) backend, abort on first failure — the suite is still +# stabilizing and a single early failure is enough signal to investigate +# without scrolling through hundreds of subsequent passes. +pytest_extra=() +if [[ "${CCCL_PYTHON_USE_V2:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + pytest_extra+=(-x) +fi + +cd "/home/coder/cccl/python/cuda_cccl/tests/" +if [[ "${CCCL_PYTHON_USE_V2:-}" =~ ^(1|true|TRUE|on|ON)$ ]]; then + # The test isolates itself in a fresh subprocess (LLVM initialization is + # process-wide and only cold once), but it carries the free_threading marker, + # so it must be selected by node-id here or the sweeps below never run it. + python -m pytest "${pytest_extra[@]}" -n 0 -v \ + compute/test_free_threading_stress.py::test_v2_concurrent_cold_llvm_initialization +fi +python -m pytest "${pytest_extra[@]}" -n 6 -v compute/ -m "not large and not free_threading" +python -m pytest "${pytest_extra[@]}" -n 0 -v compute/ -m "large and not free_threading" diff --git a/cccl_upstream/ci/test_cuda_compute_python_v2.sh b/cccl_upstream/ci/test_cuda_compute_python_v2.sh new file mode 100755 index 00000000..bd4dce57 --- /dev/null +++ b/cccl_upstream/ci/test_cuda_compute_python_v2.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Run the cuda.compute pytest suite against a wheel built with the v2 +# (HostJIT) backend. Mirrors test_cuda_compute_python.sh; the only difference +# is exporting CCCL_PYTHON_USE_V2 so the wheel build (and downstream pytest) +# uses cccl.c.parallel.v2. +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CCCL_PYTHON_USE_V2=1 +exec "$ci_dir/test_cuda_compute_python.sh" "$@" diff --git a/cccl_upstream/ci/test_cudax.sh b/cccl_upstream/ci/test_cudax.sh new file mode 100755 index 00000000..7d9dac03 --- /dev/null +++ b/cccl_upstream/ci/test_cudax.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +./build_cudax.sh "$@" + +PRESET="cudax" + +test_preset "CUDA Experimental" "${PRESET}" + +print_time_summary diff --git a/cccl_upstream/ci/test_libcudacxx.sh b/cccl_upstream/ci/test_libcudacxx.sh new file mode 100755 index 00000000..5a6d28b7 --- /dev/null +++ b/cccl_upstream/ci/test_libcudacxx.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +print_environment_details + +PRESET="libcudacxx" +CMAKE_OPTIONS=("-DCMAKE_CXX_STANDARD=${CXX_STANDARD}" "-DCMAKE_CUDA_STANDARD=${CXX_STANDARD}") + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + producer_id="$(util/workflow/get_producer_id.sh)" + artifact="z_libcudacxx-test-artifacts-${DEVCONTAINER_NAME:?}-$producer_id" + run_command "📦 Unpacking artifact '$artifact'" \ + "${ci_dir}/util/artifacts/download_packed.sh" "$artifact" /home/coder/cccl/ +else + "${ci_dir}/build_libcudacxx.sh" "$@" + configure_preset libcudacxx "$PRESET" "${CMAKE_OPTIONS[@]}" +fi + +test_preset "libcudacxx (CTest)" "libcudacxx-ctest" + +sccache -z > /dev/null || : + +lit_test_name="libcudacxx (lit)" +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + export LIT_OPTS="${LIT_OPTS:+${LIT_OPTS} }-Dtest_executable_mode=replay" + lit_test_name="libcudacxx (lit replay)" +fi +test_preset "${lit_test_name}" "libcudacxx-lit" + +sccache --show-adv-stats || : + +print_time_summary diff --git a/cccl_upstream/ci/test_nvbench_helper.sh b/cccl_upstream/ci/test_nvbench_helper.sh new file mode 100755 index 00000000..42193be2 --- /dev/null +++ b/cccl_upstream/ci/test_nvbench_helper.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# shellcheck source=ci/build_common.sh +source "$(dirname "${BASH_SOURCE[0]}")/build_common.sh" + +print_environment_details + +PRESET="nvbench-helper" + +CMAKE_OPTIONS=() + +GPU_REQUIRED="true" + +configure_and_build_preset "NVBench Helper" "$PRESET" "${CMAKE_OPTIONS[@]}" +test_preset "NVBench Helper" "$PRESET" "$GPU_REQUIRED" + +print_time_summary diff --git a/cccl_upstream/ci/test_packaging.sh b/cccl_upstream/ci/test_packaging.sh new file mode 100755 index 00000000..97fea776 --- /dev/null +++ b/cccl_upstream/ci/test_packaging.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(dirname "${BASH_SOURCE[0]}")" +cccl_dir="$(realpath "${ci_dir}/..")" + +MIN_CMAKE=false +minimum_cmake_version=3.18.0 + +new_args="$("${ci_dir}/util/extract_switches.sh" -min-cmake -- "$@")" +declare -a new_args="(${new_args})" +set -- "${new_args[@]}" +while true; do + case "$1" in + -min-cmake) + MIN_CMAKE=true + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + +if $MIN_CMAKE; then + echo "Installing minimum CMake version v${minimum_cmake_version}..." + wget -q \ + https://github.com/Kitware/CMake/releases/download/v"${minimum_cmake_version}"/cmake-"${minimum_cmake_version}"-Linux-x86_64.sh \ + -O /tmp/cmake-install.sh + prefix=/tmp/cmake-${minimum_cmake_version} + mkdir -p "${prefix}" + bash /tmp/cmake-install.sh --skip-license --prefix="${prefix}" + export MIN_CTEST_COMMAND="${prefix}/bin/ctest" +fi + +# Needs to happen after cmake is installed: +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" +cd "${ci_dir}" + +print_environment_details + +PRESET="packaging" + +CMAKE_OPTIONS=() + +GPU_REQUIRED="true" + +CMAKE_OPTIONS=("-DCCCL_EXAMPLE_CPM_REPOSITORY=${cccl_dir}") + +# Local -- build against the current repo's HEAD commit: +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + CMAKE_OPTIONS+=("-DCCCL_EXAMPLE_CPM_TAG=HEAD") +else + CMAKE_OPTIONS+=("-DCCCL_EXAMPLE_CPM_TAG=${GITHUB_SHA:?}") +fi + +if [[ -n "${MIN_CTEST_COMMAND:-}" ]]; then + CMAKE_OPTIONS+=("-DCCCL_EXAMPLE_CTEST_COMMAND=${MIN_CTEST_COMMAND}") +fi + +configure_and_build_preset "Packaging" "$PRESET" "${CMAKE_OPTIONS[@]}" +test_preset "Packaging" "$PRESET" "$GPU_REQUIRED" + +print_time_summary diff --git a/cccl_upstream/ci/test_python_common.sh b/cccl_upstream/ci/test_python_common.sh new file mode 100644 index 00000000..e18d9b54 --- /dev/null +++ b/cccl_upstream/ci/test_python_common.sh @@ -0,0 +1,30 @@ +set -euo pipefail + +function list_environment { + begin_group "⚙️ Existing site-packages" + pip freeze + end_group "⚙️ Existing site-packages" +} + +function run_tests { + module=$1 + + pushd "../python/${module}" >/dev/null + + TEMP_VENV_DIR="/tmp/${module}_venv" + rm -rf "${TEMP_VENV_DIR}" + python -m venv "${TEMP_VENV_DIR}" + # shellcheck disable=SC1091 + . "${TEMP_VENV_DIR}/bin/activate" + echo 'cuda-cccl @ file:///home/coder/cccl/python/cuda_cccl' > /tmp/cuda-cccl_constraints.txt + run_command "⚙️ Pip install ${module}" pip install -c /tmp/cuda-cccl_constraints.txt ".[test]" + begin_group "⚙️ ${module} site-packages" + pip freeze + end_group "⚙️ ${module} site-packages" + run_command "🚀 Pytest ${module}" pytest -n "${PARALLEL_LEVEL:-$(nproc --all --ignore=1)}" -v ./tests + deactivate + + popd >/dev/null + + print_time_summary +} diff --git a/cccl_upstream/ci/test_thrust.sh b/cccl_upstream/ci/test_thrust.sh new file mode 100755 index 00000000..1b69622e --- /dev/null +++ b/cccl_upstream/ci/test_thrust.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +set -euo pipefail + +CPU_ONLY=false +GPU_ONLY=false + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +new_args="$("${ci_dir}/util/extract_switches.sh" -cpu-only -gpu-only -- "$@")" +declare -a new_args="(${new_args})" +set -- "${new_args[@]}" +while true; do + case "$1" in + -cpu-only) + ARTIFACT_TAG="test_cpu" + CPU_ONLY=true + shift + ;; + -gpu-only) + ARTIFACT_TAG="test_gpu" + GPU_ONLY=true + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + +# shellcheck source=ci/build_common.sh +source "${ci_dir}/build_common.sh" + +print_environment_details + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + if ! $CPU_ONLY && ! $GPU_ONLY; then + echo "Error: test_thrust.sh requires -cpu-only or -gpu-only in CI" >&2 + exit 1 + fi + producer_id="$(util/workflow/get_producer_id.sh)" + run_command "📦 Unpacking test artifacts" \ + "${ci_dir}/util/artifacts/download_packed.sh" \ + "z_thrust-test-artifacts-${DEVCONTAINER_NAME:?}-$producer_id-$ARTIFACT_TAG" \ + /home/coder/cccl/ +else + ./build_thrust.sh "$@" +fi + +declare -a PRESET_GPU_PAIRS=() + +if $CPU_ONLY; then + PRESET_GPU_PAIRS+=("thrust-cpu:false") +elif $GPU_ONLY; then + PRESET_GPU_PAIRS+=("thrust-gpu:true") +else + PRESET_GPU_PAIRS+=("thrust-cpu:false" "thrust-gpu:true") +fi + +for pair in "${PRESET_GPU_PAIRS[@]}"; do + PRESET="${pair%%:*}" + GPU_REQUIRED="${pair##*:}" + test_preset "Thrust (${PRESET})" "${PRESET}" "${GPU_REQUIRED}" +done + +print_time_summary diff --git a/cccl_upstream/ci/update_rapids_version.sh b/cccl_upstream/ci/update_rapids_version.sh new file mode 100755 index 00000000..b5804b71 --- /dev/null +++ b/cccl_upstream/ci/update_rapids_version.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Copyright (c) 2024-2025, NVIDIA CORPORATION. +########################## +# RAPIDS Version Updater # +########################## + +## Usage +# bash update_rapids_version.sh + +# Format is YY.MM.PP - no leading 'v' or trailing 'a' +NEXT_FULL_TAG=$1 + +#Get . for next version +NEXT_MAJOR=$(echo "$NEXT_FULL_TAG" | awk '{split($0, a, "."); print a[1]}') +NEXT_MINOR=$(echo "$NEXT_FULL_TAG" | awk '{split($0, a, "."); print a[2]}') +# shellcheck disable=SC2034 +NEXT_PATCH=$(echo "$NEXT_FULL_TAG" | awk '{split($0, a, "."); print a[3]}') +NEXT_SHORT_TAG=${NEXT_MAJOR}.${NEXT_MINOR} + +# Need to distutils-normalize the versions for some use cases +NEXT_SHORT_TAG_PEP440=$(python -c "from packaging.version import Version; print(Version('${NEXT_SHORT_TAG}'))") + +echo "Updating RAPIDS and devcontainers to $NEXT_FULL_TAG" + +# Inplace sed replace; workaround for Linux and Mac +function sed_runner() { + sed -i.bak ''"$1"'' "$2" && rm -f "${2}".bak +} + +# Update CI files +sed_runner "/devcontainer_version/ s/'[0-9.]*'/'${NEXT_SHORT_TAG}'/g" ci/matrix.yaml +sed_runner "/devcontainer_version=/ s/=[0-9.]*/=${NEXT_SHORT_TAG}/g" ci/build_cuda_cccl_python.sh + +function update_devcontainer() { + sed_runner "s@rapidsai/devcontainers:[0-9.]*@rapidsai/devcontainers:${NEXT_SHORT_TAG}@g" "${1}" + sed_runner "s@rapidsai/devcontainers/features/rapids-build-utils:[0-9.]*@rapidsai/devcontainers/features/rapids-build-utils:${NEXT_SHORT_TAG_PEP440}@" "${1}" + sed_runner "s@\${localWorkspaceFolderBasename}-rapids-[0-9.]*@\${localWorkspaceFolderBasename}-rapids-${NEXT_SHORT_TAG}@g" "${1}" +} + +# Update .devcontainer files +find .devcontainer/ ci/rapids/ -type f -name devcontainer.json -print0 | while IFS= read -r -d '' filename; do + update_devcontainer "${filename}" +done diff --git a/cccl_upstream/ci/update_version.sh b/cccl_upstream/ci/update_version.sh new file mode 100755 index 00000000..cd996b24 --- /dev/null +++ b/cccl_upstream/ci/update_version.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# Usage: ./update_version.sh [--dry-run] +# Example: ./update_version.sh --dry-run 2 2 1 + +# Run in root cccl/ +cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit + +DRY_RUN=false + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; ;; + *) break ;; + esac + shift +done + +major="$1" +minor="$2" +patch="$3" + +if [[ -z "$major" ]] || [[ -z "$minor" ]] || [[ -z "$patch" ]]; then + echo "Usage: $0 [--dry-run] " + exit 1 +fi + +# Version file paths +REPO_VERSION_FILE="cccl-version.json" +DOCS_VERSION_MD_FILE="docs/VERSION.md" +CCCL_VERSION_FILE="libcudacxx/include/cuda/std/__cccl/version.h" +THRUST_VERSION_FILE="thrust/thrust/version.h" +CUB_VERSION_FILE="cub/cub/version.cuh" +CCCL_CMAKE_VERSION_FILE="lib/cmake/cccl/cccl-config-version.cmake" +CUB_CMAKE_VERSION_FILE="lib/cmake/cub/cub-config-version.cmake" +LIBCUDACXX_CMAKE_VERSION_FILE="lib/cmake/libcudacxx/libcudacxx-config-version.cmake" +THRUST_CMAKE_VERSION_FILE="lib/cmake/thrust/thrust-config-version.cmake" +CUDAX_CMAKE_VERSION_FILE="lib/cmake/cudax/cudax-config-version.cmake" +CUDA_CCCL_VERSION_FILE="python/cuda_cccl/cuda/cccl/_version.py" + +# Calculated version codes +new_cccl_version=$((major * 1000000 + minor * 1000 + patch)) # MMMmmmppp +new_thrust_cub_version=$((major * 100000 + minor * 100 + patch)) # MMMmmmpp + +# Fetch current version from file +current_cccl_version=$(grep -oP "define CCCL_VERSION \K[0-9]+" "$CCCL_VERSION_FILE") + +# Fetch the latest tag from git and strip the 'v' prefix if present +latest_tag=$(git tag --sort=-v:refname | head -n 1 | sed 's/^v//') + +# Since the tags and versions are numerically comparable, we cast them to integers +latest_tag_version=$(echo "$latest_tag" | awk -F. '{ printf("%d%03d%03d", $1,$2,$3) }') + +echo "Running in $(pwd)" +echo "New MMMmmmppp version: $new_cccl_version" +echo "New MMMmmmpp version: $new_thrust_cub_version" +echo "Current CCCL version: $current_cccl_version" +echo "Latest git tag: $latest_tag" + +# Check if new version is less than or equal to current or the latest tag +if (( new_cccl_version < current_cccl_version )) || (( new_cccl_version < latest_tag_version )); then + echo "Error: New version $new_cccl_version is less than current version $current_cccl_version or latest git tag version $latest_tag_version." + exit 1 +fi + +update_file () { + local file=$1 + local pattern=$2 + local new_value=$3 + if [[ "$DRY_RUN" = true ]]; then + local temp_file + temp_file=$(mktemp) + sed "s/$pattern/$new_value/g" "$file" > "$temp_file" + diff --color=auto -U 0 "$file" "$temp_file" || true + rm "$temp_file" + else + sed -i "s/$pattern/$new_value/" "$file" + fi +} + +# Update version information in files: + +update_file "$REPO_VERSION_FILE" " \"full\":.*," " \"full\": \"$major.$minor.$patch\"," +update_file "$REPO_VERSION_FILE" " \"major\":.*" " \"major\": $major," +update_file "$REPO_VERSION_FILE" " \"minor\":.*" " \"minor\": $minor," +update_file "$REPO_VERSION_FILE" " \"patch\":.*" " \"patch\": $patch" + +update_file "$DOCS_VERSION_MD_FILE" ".*" "$major.$minor" + +update_file "$CCCL_VERSION_FILE" "^#define CCCL_VERSION \([0-9]\+\)" "#define CCCL_VERSION $new_cccl_version" +update_file "$THRUST_VERSION_FILE" "^#define THRUST_VERSION \([0-9]\+\)" "#define THRUST_VERSION $new_thrust_cub_version" +update_file "$CUB_VERSION_FILE" "^#define CUB_VERSION \([0-9]\+\)" "#define CUB_VERSION $new_thrust_cub_version" + +update_file "$CUB_CMAKE_VERSION_FILE" "set(CUB_VERSION_MAJOR \([0-9]\+\))" "set(CUB_VERSION_MAJOR $major)" +update_file "$CUB_CMAKE_VERSION_FILE" "set(CUB_VERSION_MINOR \([0-9]\+\))" "set(CUB_VERSION_MINOR $minor)" +update_file "$CUB_CMAKE_VERSION_FILE" "set(CUB_VERSION_PATCH \([0-9]\+\))" "set(CUB_VERSION_PATCH $patch)" + +update_file "$LIBCUDACXX_CMAKE_VERSION_FILE" "set(libcudacxx_VERSION_MAJOR \([0-9]\+\))" "set(libcudacxx_VERSION_MAJOR $major)" +update_file "$LIBCUDACXX_CMAKE_VERSION_FILE" "set(libcudacxx_VERSION_MINOR \([0-9]\+\))" "set(libcudacxx_VERSION_MINOR $minor)" +update_file "$LIBCUDACXX_CMAKE_VERSION_FILE" "set(libcudacxx_VERSION_PATCH \([0-9]\+\))" "set(libcudacxx_VERSION_PATCH $patch)" + +update_file "$THRUST_CMAKE_VERSION_FILE" "set(THRUST_VERSION_MAJOR \([0-9]\+\))" "set(THRUST_VERSION_MAJOR $major)" +update_file "$THRUST_CMAKE_VERSION_FILE" "set(THRUST_VERSION_MINOR \([0-9]\+\))" "set(THRUST_VERSION_MINOR $minor)" +update_file "$THRUST_CMAKE_VERSION_FILE" "set(THRUST_VERSION_PATCH \([0-9]\+\))" "set(THRUST_VERSION_PATCH $patch)" + +update_file "$CCCL_CMAKE_VERSION_FILE" "set(CCCL_VERSION_MAJOR \([0-9]\+\))" "set(CCCL_VERSION_MAJOR $major)" +update_file "$CCCL_CMAKE_VERSION_FILE" "set(CCCL_VERSION_MINOR \([0-9]\+\))" "set(CCCL_VERSION_MINOR $minor)" +update_file "$CCCL_CMAKE_VERSION_FILE" "set(CCCL_VERSION_PATCH \([0-9]\+\))" "set(CCCL_VERSION_PATCH $patch)" + +update_file "$CUDAX_CMAKE_VERSION_FILE" "set(cudax_VERSION_MAJOR \([0-9]\+\))" "set(cudax_VERSION_MAJOR $major)" +update_file "$CUDAX_CMAKE_VERSION_FILE" "set(cudax_VERSION_MINOR \([0-9]\+\))" "set(cudax_VERSION_MINOR $minor)" +update_file "$CUDAX_CMAKE_VERSION_FILE" "set(cudax_VERSION_PATCH \([0-9]\+\))" "set(cudax_VERSION_PATCH $patch)" + +update_file "$CUDA_CCCL_VERSION_FILE" "^__version__ = \"\([0-9.]\+\)\"" "__version__ = \"$major.$minor.$patch\"" + + +if [[ "$DRY_RUN" = true ]]; then + echo "Dry run completed. No changes made." +else + echo "Version updated to $major.$minor.$patch" +fi diff --git a/cccl_upstream/ci/upload_cub_test_artifacts.sh b/cccl_upstream/ci/upload_cub_test_artifacts.sh new file mode 100755 index 00000000..b734c292 --- /dev/null +++ b/cccl_upstream/ci/upload_cub_test_artifacts.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ci_dir +repo_root="$(cd "${ci_dir}/.." && pwd)" +readonly repo_root + +cd "$repo_root" + +if ! ci/util/workflow/has_consumers.sh; then + echo "No consumers found for this job. Exiting." >&2 + exit 0 +fi + +if [[ "$#" -gt 0 ]]; then + preset_variants=("$@") +else + # Figure out which artifacts need to be built: + consumers=$(ci/util/workflow/get_consumers.sh) + preset_variants=() + if grep -q "TestGPU" <<< "$consumers"; then + preset_variants+=("no_lid") + fi + if grep -q "HostLaunch" <<< "$consumers"; then + preset_variants+=("lid_0") + fi + if grep -q "DeviceLaunch" <<< "$consumers"; then + preset_variants+=("lid_1") + fi + if grep -q "GraphCapture" <<< "$consumers"; then + preset_variants+=("lid_2") + fi + # Limited jobs run the entire test suite: + if grep -q "SmallGMem" <<< "$consumers"; then + preset_variants+=("no_lid" "lid_0" "lid_1" "lid_2") + fi +fi + +# Remove duplicates: +mapfile -t preset_variants < <(echo "${preset_variants[*]}" | tr ' ' '\n' | sort -u) + +artifact_prefix="z_cub-test-artifacts-${DEVCONTAINER_NAME:?}-${JOB_ID:?}" + +# BUILD_INFIX is undefined on windows CI +build_dir_regex="build${CCCL_BUILD_INFIX:+/$CCCL_BUILD_INFIX}/cub[^/]*" + +# Just collect the minimum set of files needed for running each ctest preset: +for preset_variant in "${preset_variants[@]}"; do + + # Shared across all presets: + ci/util/artifacts/stage.sh "$artifact_prefix-$preset_variant" \ + "$build_dir_regex/build\.ninja$" \ + "$build_dir_regex/.*rules\.ninja$" \ + "$build_dir_regex/CMakeCache\.txt$" \ + "$build_dir_regex/.*VerifyGlobs\.cmake$" \ + "$build_dir_regex/.*CTestTestfile\.cmake$" \ + "$build_dir_regex/cub/rapids-cmake/.*" > /dev/null + + # Add per-preset executables: + if [[ "$preset_variant" == lid_* ]]; then + ci/util/artifacts/stage.sh \ + "$artifact_prefix-$preset_variant" \ + "$build_dir_regex/bin/.*\.$preset_variant.*" > /dev/null + # The CUDA runtime smoke binary is invoked explicitly from build_common.sh + ci/util/artifacts/stage.sh \ + "$artifact_prefix-$preset_variant" \ + "$build_dir_regex/bin/cccl\.test\.cuda_runtime_smoke$" > /dev/null + # Windows builds generate binaries for the header tests, remove these: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-$preset_variant" \ + "$build_dir_regex/.*\.headers\..*" > /dev/null || : + + ci/util/artifacts/upload_stage_packed.sh "$artifact_prefix-$preset_variant" + fi +done + +if [[ " ${preset_variants[*]} " == *" no_lid "* ]]; then + # Initially add all binaries to no_lid, then remove the lid variants in later passes: + ci/util/artifacts/stage.sh \ + "$artifact_prefix-no_lid" \ + "$build_dir_regex/bin/.*" > /dev/null + # Remove the benchmarks if present, we don't run those in CI, just build them: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-no_lid" \ + "$build_dir_regex/.*\.bench\..*" > /dev/null || : + # Remove all lid variants: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-no_lid" \ + "$build_dir_regex/.*\.lid_[0-2].*" > /dev/null + # Windows builds generate binaries for the header tests, remove these: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-no_lid" \ + "$build_dir_regex/.*\.headers\..*" > /dev/null || : + # These cubin outputs are needed for FileCheck tests in test/cubin-check + ci/util/artifacts/stage.sh \ + "$artifact_prefix-no_lid" \ + "$build_dir_regex/cub/test/cubin-check/.*\.cubin$" > /dev/null || : + + ci/util/artifacts/upload_stage_packed.sh "$artifact_prefix-no_lid" +fi diff --git a/cccl_upstream/ci/upload_job_result_artifacts.sh b/cccl_upstream/ci/upload_job_result_artifacts.sh new file mode 100755 index 00000000..a73bba1e --- /dev/null +++ b/cccl_upstream/ci/upload_job_result_artifacts.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ci_dir +repo_root="$(cd "${ci_dir}/.." && pwd)" +readonly repo_root + +cd "$repo_root" + +usage=$(cat < +EOF +) + +if [[ "$#" -ne 2 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="$1" +exit_code="$2" + +# Collect workflow-related artifacts -- success state, sccache info, etc. +# These are unpacked and parsed in the workflow-results action. + +source ci/util/artifacts/common.sh + +# The root of the shared artifact structure: +jobs_artifact_dir="$ARTIFACT_UPLOAD_STAGE/jobs" + +# This job's artifact directory: +job_artifacts="$jobs_artifact_dir/$job_id" +mkdir -p "$job_artifacts" + +if [[ "$exit_code" -eq 0 ]]; then + touch "$job_artifacts/success" +fi + +# Finds a matching file in the root and copies it to the artifact directory. +find_and_copy_job_artifact_from() { + root="$1" + name="$2" + if find "$root"/ -maxdepth 4 -name "$name" -type f -printf '' -quit 2>/dev/null; then + find "$root"/ -maxdepth 4 -name "$name" -type f -print0 | xargs -0 -P4 -I% cp -v % "$job_artifacts"/ + else + echo "No file matching '$name' found in '$root'." + return 1 + fi +} + +find_and_copy_job_artifact_from /tmp "sccache*.log" || : # Nonfatal if not found +find_and_copy_job_artifact_from build "sccache_stats.json" || : # Nonfatal if not found +find_and_copy_job_artifact_from build ".ninja_log" || : # Nonfatal if not found +find_and_copy_job_artifact_from build "build.ninja" || : # Nonfatal if not found +find_and_copy_job_artifact_from build "rules.ninja" || : # Nonfatal if not found +find_and_copy_job_artifact_from build "ctest.log" || : # Nonfatal if not found + +ci/util/artifacts/upload/register.sh "zz_jobs-$job_id" "$jobs_artifact_dir" diff --git a/cccl_upstream/ci/upload_libcudacxx_test_artifacts.sh b/cccl_upstream/ci/upload_libcudacxx_test_artifacts.sh new file mode 100755 index 00000000..34e2248c --- /dev/null +++ b/cccl_upstream/ci/upload_libcudacxx_test_artifacts.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ci_dir +repo_root="$(cd "${ci_dir}/.." && pwd)" +readonly repo_root + +cd "$repo_root" + +if ! ci/util/workflow/has_consumers.sh; then + echo "No consumers found for this job. Exiting." >&2 + exit 0 +fi + +artifact="z_libcudacxx-test-artifacts-${DEVCONTAINER_NAME:?}-${JOB_ID:?}" + +# BUILD_INFIX is undefined on windows CI. +build_dir_regex="build${CCCL_BUILD_INFIX:+/$CCCL_BUILD_INFIX}/libcudacxx[^/]*" +lit_executable_regex="$build_dir_regex/libcudacxx/test/libcudacxx/test/.*Output/.*\.exe$" + +# Minimum CTest/lit metadata needed to run from the unpacked build tree. +ci/util/artifacts/stage.sh "$artifact" \ + "$build_dir_regex/build\.ninja$" \ + "$build_dir_regex/.*rules\.ninja$" \ + "$build_dir_regex/CMakeCache\.txt$" \ + "$build_dir_regex/.*VerifyGlobs\.cmake$" \ + "$build_dir_regex/.*CTestTestfile\.cmake$" \ + "$build_dir_regex/libcudacxx/test/libcudacxx/lit\.site\.cfg$" \ + > /dev/null + +# Test executables plus shared libraries/smoke binaries used by CTest/lit. +ci/util/artifacts/stage.sh "$artifact" \ + "$build_dir_regex/bin/.*" \ + "$build_dir_regex/lib/.*" \ + "$lit_executable_regex" \ + > /dev/null + +if ! find . -type f -regex "\./$lit_executable_regex" -print -quit | grep -q .; then + echo "No lit test executables found for artifact '$artifact'." >&2 + exit 1 +fi + +# Windows builds generate binaries for header tests that are never executed. +ci/util/artifacts/unstage.sh \ + "$artifact" \ + "$build_dir_regex/.*\.headers\..*" > /dev/null || : + +ci/util/artifacts/upload_stage_packed.sh "$artifact" diff --git a/cccl_upstream/ci/upload_thrust_test_artifacts.sh b/cccl_upstream/ci/upload_thrust_test_artifacts.sh new file mode 100755 index 00000000..2065abe1 --- /dev/null +++ b/cccl_upstream/ci/upload_thrust_test_artifacts.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ci_dir +repo_root="$(cd "${ci_dir}/.." && pwd)" +readonly repo_root + +cd "$repo_root" + +if ! ci/util/workflow/has_consumers.sh; then + echo "No consumers found for this job. Exiting." >&2 + exit 0 +fi + +# Figure out which artifact needs to be built: +consumers=$(ci/util/workflow/get_consumers.sh) +preset_variants=() +if grep -q "TestGPU" <<< "$consumers"; then + preset_variants+=("test_gpu") +fi +if grep -q "TestCPU" <<< "$consumers"; then + preset_variants+=("test_cpu") +fi + +artifact_prefix="z_thrust-test-artifacts-${DEVCONTAINER_NAME:?}-${JOB_ID:?}" + +# BUILD_INFIX is undefined on windows CI +build_dir_regex="build${CCCL_BUILD_INFIX:+/$CCCL_BUILD_INFIX}/thrust[^/]*" + +# Just collect the minimum set of files needed for running each ctest preset: +for preset_variant in "${preset_variants[@]}"; do + # Shared across all presets: + ci/util/artifacts/stage.sh "$artifact_prefix-$preset_variant" \ + "$build_dir_regex/build\.ninja$" \ + "$build_dir_regex/.*rules\.ninja$" \ + "$build_dir_regex/CMakeCache\.txt$" \ + "$build_dir_regex/.*VerifyGlobs\.cmake$" \ + "$build_dir_regex/.*CTestTestfile\.cmake$" \ + > /dev/null +done + +if [[ " ${preset_variants[*]} " == *" test_cpu "* ]]; then + # Initially add all binaries, then remove all containing 'cuda' in the name: + ci/util/artifacts/stage.sh \ + "$artifact_prefix-test_cpu" \ + "$build_dir_regex/bin/.*" > /dev/null + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-test_cpu" \ + "$build_dir_regex/bin/thrust\..*\.cuda\..*" > /dev/null + + ci/util/artifacts/stage.sh \ + "$artifact_prefix-test_cpu" \ + "$build_dir_regex/lib/.*\.test\.framework\..*" > /dev/null + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-test_cpu" \ + "$build_dir_regex/lib/.*\.cuda\.test\.framework\..*" > /dev/null + + # Windows builds generate binaries for the header tests, remove these: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-test_cpu" \ + "$build_dir_regex/.*\.headers\..*" > /dev/null || : + + ci/util/artifacts/upload_stage_packed.sh "$artifact_prefix-test_cpu" +fi + +if [[ " ${preset_variants[*]} " == *" test_gpu "* ]]; then + # Only binaries containing 'cuda': + ci/util/artifacts/stage.sh \ + "$artifact_prefix-test_gpu" \ + "$build_dir_regex/bin/thrust\..*\.cuda\..*" > /dev/null + ci/util/artifacts/stage.sh \ + "$artifact_prefix-test_gpu" \ + "$build_dir_regex/lib/.*\.cuda\.test\.framework\..*" > /dev/null + # The CUDA runtime smoke binary is invoked explicitly from build_common.sh + ci/util/artifacts/stage.sh \ + "$artifact_prefix-test_gpu" \ + "$build_dir_regex/bin/cccl\.test\.cuda_runtime_smoke$" > /dev/null + + # Windows builds generate binaries for the header tests, remove these: + ci/util/artifacts/unstage.sh \ + "$artifact_prefix-test_gpu" \ + "$build_dir_regex/.*\.headers\..*" > /dev/null || : + + + ci/util/artifacts/upload_stage_packed.sh "$artifact_prefix-test_gpu" +fi diff --git a/cccl_upstream/ci/util/artifacts/common.sh b/cccl_upstream/ci/util/artifacts/common.sh new file mode 100755 index 00000000..d7c72013 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/common.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "This script must be sourced, not executed directly." >&2 + exit 1 +fi + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +to_posix_path() { + local path="$1" + + if [[ "$path" =~ ^([A-Za-z]):([\\/]?.*)$ ]]; then + local drive="${BASH_REMATCH[1]}" + local rest="${BASH_REMATCH[2]}" + rest="${rest//\\/\/}" + printf '/%s%s\n' "${drive,,}" "$rest" + return + fi + + printf '%s\n' "$path" +} + +runner_temp_posix="$(to_posix_path "${RUNNER_TEMP:-/tmp}")" + +export ARTIFACT_UPLOAD_STAGE="${runner_temp_posix}/artifact_upload_stage" +export ARTIFACT_ARCHIVES="${runner_temp_posix}/artifact_archives" +export ARTIFACT_UPLOAD_REGISTERY="${ARTIFACT_UPLOAD_STAGE}/artifact_upload_registry.json" + +mkdir -p "$ARTIFACT_UPLOAD_STAGE" "$ARTIFACT_ARCHIVES" + +if [[ ! -f "$ARTIFACT_UPLOAD_REGISTERY" ]]; then + echo "[]" > "$ARTIFACT_UPLOAD_REGISTERY" +fi diff --git a/cccl_upstream/ci/util/artifacts/download.sh b/cccl_upstream/ci/util/artifacts/download.sh new file mode 100755 index 00000000..96f2ca01 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/download.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [] + +Download artifacts uploaded by other jobs in this CI run. + +Example Usage: + Download an artifact to the current directory: + $0 source_artifact.tar.gz + + Download a packed artifact and extract it to the provided path: + $0 job-\$ID-products some/path/ +EOF +) +readonly usage + +if [[ "$#" -lt 1 ]]; then + echo "Error: Missing artifact name." >&2 + echo "$usage" >&2 + exit 1 +fi +readonly artifact_name="$1" + +if [[ "$#" -eq 1 ]]; then + artifact_path="./" +else + artifact_path="$2" +fi + +start=$SECONDS +"$ci_dir/util/artifacts/download/fetch.sh" "$artifact_name" "$artifact_path" +echo "Artifact '$artifact_name' downloaded to '$artifact_path' in $((SECONDS - start)) seconds." diff --git a/cccl_upstream/ci/util/artifacts/download/fetch.sh b/cccl_upstream/ci/util/artifacts/download/fetch.sh new file mode 100755 index 00000000..7694fe71 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/download/fetch.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Downloads files from a named artifact from the current CI workflow run into the specified directory. + +Example Usages: + - $0 my_artifact.tar.gz ./ + - $0 my_artifact /path/to/some/directory/ +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" + +# Create the target directory and then get its absolute path +mkdir -p "$2" +target_directory="$(cd "$2" && pwd)" +readonly target_directory + +echo "Downloading artifact '$artifact_name' to '$target_directory'" +# shellcheck disable=SC2154 +"$ci_dir/util/retry.sh" 5 30 \ + gh run download "${GITHUB_RUN_ID}" \ + --name "$artifact_name" \ + --dir "$target_directory" diff --git a/cccl_upstream/ci/util/artifacts/download/unpack.sh b/cccl_upstream/ci/util/artifacts/download/unpack.sh new file mode 100755 index 00000000..84c6f103 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/download/unpack.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Unpacks a fetched packed artifact's tar.zst archive into the specified directory. + +Example Usages: + - $0 /tmp/my_artifact.tar.zst /tmp/my_artifact + - $0 /path/to/archive.tar.zst /path/to/extract/ +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +if ! command -v zstd > /dev/null 2>&1; then + echo "Error: zstd not found." >&2 + exit 1 +fi + +readonly artifact_name="$1" +readonly artifact_path="$2" + +readonly artifact_archive="$ARTIFACT_ARCHIVES/${artifact_name}.tar.zst" + +echo "Unpacking artifact from '$artifact_archive' to '$artifact_path'" +echo "Using zstd executable: $(command -v zstd)" + +# Create the artifact path directory if it doesn't exist +mkdir -p "$artifact_path" + +zstd --decompress --threads=0 --stdout "$artifact_archive" \ + | tar -xv -C "$artifact_path" diff --git a/cccl_upstream/ci/util/artifacts/download_packed.sh b/cccl_upstream/ci/util/artifacts/download_packed.sh new file mode 100755 index 00000000..5cf4a6f7 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/download_packed.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [] + +Download and extracts a packed artifact uploaded by another job in this CI run. + +Example Usage: + Download an artifact to the current directory: + $0 source_artifact.tar.gz + + Download a packed artifact and extract it to the provided path: + $0 job-\$ID-products build/ +EOF +) +readonly usage + +if [[ "$#" -lt 1 ]]; then + echo "Error: Missing artifact name." >&2 + echo "$usage" >&2 + exit 1 +fi +readonly artifact_name="$1" + +if [[ "$#" -eq 1 ]]; then + artifact_path="./" +else + artifact_path="$2" +fi + +start=$SECONDS +"$ci_dir/util/artifacts/download/fetch.sh" "$artifact_name" "${ARTIFACT_ARCHIVES}" +fetched=$SECONDS +"$ci_dir/util/artifacts/download/unpack.sh" "$artifact_name" "$artifact_path" +unpacked=$SECONDS + +echo "Artifact '$artifact_name' fetched in $((fetched - start)) seconds." +echo "Artifact '$artifact_name' unpacked in $((unpacked - fetched)) seconds." diff --git a/cccl_upstream/ci/util/artifacts/stage.sh b/cccl_upstream/ci/util/artifacts/stage.sh new file mode 100755 index 00000000..30eed04d --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/stage.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [ ...] + +Stages files matching the provided regexes path for upload under the specified artifact. +Regexes are passed to the 'find' command's -regex option within the artifact stage path and implicitly +start with '^\\./'. + +Staged files can be unstaged using the 'artifact/unstage.sh' script. +All stage / unstage operations on the same artifact must be performed from the same working directory. + +Once a stage is complete, 'artifact/upload_stage_packed.sh' can be used to create a packed artifact +from the stage. See also 'artifact/upload/pack.sh' and 'artifact/upload/build.sh' for more staging options. + +Example Usage: + +Stage built binaries and .cmake files in \${ARTIFACT_UPLOAD_STAGE}/test_artifacts for upload: + $0 test_artifacts 'bin/.*' 'lib/.*' '.*cmake$' +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +artifact_name="$1" +shift +regexes=("$@") + +artifact_stage_path="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}" +if [[ "$artifact_stage_path" != /* ]]; then + artifact_stage_path="$(pwd)/$artifact_stage_path" +fi + +mkdir -p "$artifact_stage_path" + +artifact_index_file="$artifact_stage_path/artifact_index.txt" +artifact_index_cwd="$artifact_stage_path/artifact_index_cwd.txt" + +if [[ -f "$artifact_index_cwd" ]]; then + # Check that the cwd matches the original staging directory if the index already exists: + if [[ "$(cat "$artifact_index_cwd")" != "$(pwd)" ]]; then + echo "Error: The current working directory has changed since the artifact was staged." >&2 + echo "Cannot currently stage files from multiple source directories." >&2 + exit 1 + fi +else + pwd > "$artifact_index_cwd" +fi + +echo "Staging artifacts in '$artifact_stage_path'" +for regex in "${regexes[@]}"; do + # Prepend './' to the regex for convenience. There's an implied ^ at the start of the find regex, + # and paths always start with ./, so this lets us match top level files directly. + regex="\\./$regex" + echo "Staging files matching regex: $regex" + find . -type f -regex "$regex" | tee -a "$artifact_index_file" + echo +done diff --git a/cccl_upstream/ci/util/artifacts/unstage.sh b/cccl_upstream/ci/util/artifacts/unstage.sh new file mode 100755 index 00000000..ebf23507 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/unstage.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [ ...] + +Unstages (removes) files matching the provided regexes from the specified artifact stage. +Regexes follow the same rules as artifact/stage.sh. + +This may be used to remove files that were previously staged for upload before packing or building the artifacts. + +Example Usage: + +Unstage previously-staged built binaries and .cmake files from test_artifacts: + $0 test_artifacts 'bin/.*' 'lib/.*' '.*cmake$' +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +artifact_name="$1" +shift +regexes=("$@") + +artifact_stage_path="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}" +if [[ "$artifact_stage_path" != /* ]]; then + artifact_stage_path="$(pwd)/$artifact_stage_path" +fi + +mkdir -p "$artifact_stage_path" + +artifact_index_file="$artifact_stage_path/artifact_index.txt" +artifact_index_cwd="$artifact_stage_path/artifact_index_cwd.txt" + +pwd > "$artifact_index_cwd" + +echo "Unstaging artifacts in '$artifact_stage_path'" +for regex in "${regexes[@]}"; do + # Modify regex for consistency with staging script: + regex="^\\./$regex" + echo "Unstaging files matching regex: $regex" + grep -E "$regex" "$artifact_index_file" + grep -v -E "$regex" "$artifact_index_file" > "${artifact_index_file}.tmp" && \ + mv "${artifact_index_file}.tmp" "$artifact_index_file" +done diff --git a/cccl_upstream/ci/util/artifacts/upload.sh b/cccl_upstream/ci/util/artifacts/upload.sh new file mode 100755 index 00000000..2a2c7f64 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [ ...] + +Creates an artifact consisting of a zip file containing a single file or set of regex matches. + +Regexes are passed to the $(command -v find) command's -regex option in the current directory. +'./' is prepended to all regexes for convenience. +The artifact will contain all matching files with paths relative to the current directory. + +If no regexes are provided, the artifact will be created from a file in the current directory. +The file must have the same name as the artifact. + +Example Usage: + + Create an artifact of the given file in the current directory using the filename as the artifact name: + + $0 some_resource.log + + Copy all files that match the regexes to a staging directory, and upload a artifact that zips this directory. + + $0 job-\$JOB_ID-products \ + 'build\.ninja$' \ + '.*rules\.ninja$' \ + 'CMakeCache\.txt$' \ + '.*VerifyGlobs\.cmake$' \ + '.*CTestTestfile\.cmake$' \ + 'bin/.*' \ + 'lib/.*' +EOF +) +readonly usage + +if [[ "$#" -lt 1 ]]; then + echo "Error: Missing artifact name." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" + +# If no regexes are provided, use the artifact name as the path: +if [[ "$#" -eq 1 ]]; then + "$ci_dir/util/artifacts/upload/register.sh" "$artifact_name" "$artifact_name" + exit +fi + +shift + +"$ci_dir/util/artifacts/stage.sh" "$artifact_name" "$@" > /dev/null +"$ci_dir/util/artifacts/upload_stage.sh" "$artifact_name" diff --git a/cccl_upstream/ci/util/artifacts/upload/build.sh b/cccl_upstream/ci/util/artifacts/upload/build.sh new file mode 100755 index 00000000..308e1d57 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/build.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Builds a physical tree containing a staged artifact created using artifact/stage.sh / unstage.sh. + +The artifact root will be located at \${ARTIFACT_UPLOAD_STAGE}//. +EOF +) +readonly usage + +if [[ "$#" -ne 1 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" +readonly artifact_stage_path="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}" +readonly artifact_index_file="$artifact_stage_path/artifact_index.txt" +readonly artifact_cwd_file="$artifact_stage_path/artifact_index_cwd.txt" +readonly artifact_dir="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}/${artifact_name}" +artifact_cwd="$(cat "$artifact_cwd_file")" +readonly artifact_cwd + +mkdir -p "$artifact_dir" + +echo "Building artifact '$artifact_name' in '$artifact_dir'" +echo "Pulling artifacts from working directory: $artifact_cwd" + +( + cd "$artifact_cwd" + while IFS= read -r file; do + cp -v --parents "$file" "$artifact_dir" + done < "$artifact_index_file" +) diff --git a/cccl_upstream/ci/util/artifacts/upload/pack.sh b/cccl_upstream/ci/util/artifacts/upload/pack.sh new file mode 100755 index 00000000..3410a107 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/pack.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Packs a staged artifact (created using artifact/stage.sh) into a tar.zst archive. + +The archive will be generated from the staged index file in \${ARTIFACT_UPLOAD_STAGE}/ and +saved to \${ARTIFACT_UPLOAD_STAGE}//.tar.zst. + +Example Usages: + - $0 test_artifact +EOF +) +readonly usage + +if [[ "$#" -ne 1 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +if ! command -v zstd > /dev/null 2>&1; then + echo "Error: zstd not found." >&2 + exit 1 +fi + +readonly artifact_name="$1" +readonly artifact_stage_path="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}" +readonly artifact_index_file="$artifact_stage_path/artifact_index.txt" +readonly artifact_cwd_file="$artifact_stage_path/artifact_index_cwd.txt" +readonly artifact_archive="${ARTIFACT_UPLOAD_STAGE}/${artifact_name}/${artifact_name}.tar.zst" + +echo "Packing artifact '$artifact_stage_path' into '$artifact_archive'" +echo "Using zstd: $(command -v zstd)" +echo "Pulling artifacts from working directory: $(cat "$artifact_cwd_file")" + +tar -cv -C "$(cat "$artifact_cwd_file")" -T "$artifact_index_file" \ + | zstd --compress --threads=0 \ + > "$artifact_archive" diff --git a/cccl_upstream/ci/util/artifacts/upload/print_matrix.sh b/cccl_upstream/ci/util/artifacts/upload/print_matrix.sh new file mode 100755 index 00000000..7aacaad1 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/print_matrix.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat <&2 + exit 1 +fi + +jq -c '.' "${ARTIFACT_UPLOAD_REGISTERY:?}" diff --git a/cccl_upstream/ci/util/artifacts/upload/register.sh b/cccl_upstream/ci/util/artifacts/upload/register.sh new file mode 100755 index 00000000..babf3713 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/register.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +readonly artifact_compression_level=6 +readonly artifact_retention_days=7 + +usage=$(cat < [] + +Registers artifacts for upload. If path is not provided, it defaults to the artifact name. + +Compression level is set to $artifact_compression_level by default. +Use 'upload/set_compression_level.sh' to change this after registering if needed. + +Default retention days is set to $artifact_retention_days. +Use 'upload/set_retention_days.sh' after registering to change this if needed. + +Example Usages: + - $0 my_artifact.tar.gz # Assumes the artifact is in the current directory. + - $0 my_artifact /path/to/my_artifact.tar.gz + - $0 my_artifact /path/to/my_artifact_directory/ +EOF +) +readonly usage + +if [[ "$#" -lt 1 ]]; then + echo "Error: Missing artifact name." >&2 + echo "$usage" >&2 + exit 1 +fi + +artifact_name="$1" +artifact_path="${2:-$artifact_name}" + +# Ensure the artifact path is absolute +if [[ "$artifact_path" != /* ]]; then + artifact_path="$(pwd)/$artifact_path" +fi + +if [[ ! -e "$artifact_path" ]]; then + echo "Error: Artifact path '$artifact_path' does not exist." >&2 + echo "$usage" >&2 + exit 1 +fi + +# Register the artifact: +jq --arg name "$artifact_name" \ + --arg path "$artifact_path" \ + --arg retention_days "$artifact_retention_days" \ + --argjson compression_level "$artifact_compression_level" \ + '. += [{"name": $name, "path": $path, "retention_days": ($retention_days | tonumber), "compression_level": $compression_level}]' \ + "$ARTIFACT_UPLOAD_REGISTERY" > "$ARTIFACT_UPLOAD_REGISTERY.tmp" && \ + mv "$ARTIFACT_UPLOAD_REGISTERY.tmp" "$ARTIFACT_UPLOAD_REGISTERY" + +echo "Artifact '$artifact_name' registered for upload with path '$artifact_path'." diff --git a/cccl_upstream/ci/util/artifacts/upload/set_compression_level.sh b/cccl_upstream/ci/util/artifacts/upload/set_compression_level.sh new file mode 100755 index 00000000..0fbb5a01 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/set_compression_level.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Sets the compression level for an artifact registered for upload. + +Example Usage: + $0 some_huge_precompressed_archive 0 + $0 some_many_small_uncompressed_files 10 +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +artifact_name="$1" +compression_level="$2" + +# Find the artifact entry and update its compression level +jq --arg name "$artifact_name" --argjson compression_level "$compression_level" \ + 'map(if .name == $name then .compression_level = $compression_level else . end)' \ + "$ARTIFACT_UPLOAD_REGISTERY" > "$ARTIFACT_UPLOAD_REGISTERY.tmp" && \ + mv "$ARTIFACT_UPLOAD_REGISTERY.tmp" "$ARTIFACT_UPLOAD_REGISTERY" diff --git a/cccl_upstream/ci/util/artifacts/upload/set_retention_days.sh b/cccl_upstream/ci/util/artifacts/upload/set_retention_days.sh new file mode 100755 index 00000000..3db5a6c0 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload/set_retention_days.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Sets the retention days for an artifact registered for upload. + +Example Usage: + $0 some_huge_temporary_artifact 1 + $0 some_small_useful_output 7 + $0 some_long_term_artifact 30 +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Missing arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +artifact_name="$1" +retention_days="$2" + +# Find the artifact entry and update its retention days +jq --arg name "$artifact_name" --argjson retention_days "$retention_days" \ + 'map(if .name == $name then .retention_days = $retention_days else . end)' \ + "$ARTIFACT_UPLOAD_REGISTERY" > "$ARTIFACT_UPLOAD_REGISTERY.tmp" && \ + mv "$ARTIFACT_UPLOAD_REGISTERY.tmp" "$ARTIFACT_UPLOAD_REGISTERY" diff --git a/cccl_upstream/ci/util/artifacts/upload_packed.sh b/cccl_upstream/ci/util/artifacts/upload_packed.sh new file mode 100755 index 00000000..4fba3a17 --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload_packed.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < [ ...] + +Create a compressed artifact, suitable for large, temporary files such as build products or test binaries +that need to be quickly uploaded and downloaded between CI jobs. The artifact will exist of a +zip file containing an .tar.zst archive, packed with the parallel zstd. + +Regexes are passed to the $(command -v find) command's -regex option in the current directory. +'./' is prepended to all regexes for convenience. +The artifact will contain all matching files relative to the current directory. + +If no regexes are provided, the artifact will be created from a file in the current directory. +The file must have the same name as the artifact. + +Example Usage: + + Create an artifact of the given file in the current directory using the filename as the artifact name: + + $0 some_resource.log + + Copy all files that match the regexes to a staging directory, and upload a artifact that zips this directory. + + $0 job-\$JOB_ID-products \ + 'build\.ninja$' \ + '.*rules\.ninja$' \ + 'CMakeCache\.txt$' \ + '.*VerifyGlobs\.cmake$' \ + '.*CTestTestfile\.cmake$' \ + 'bin/.*' \ + 'lib/.*' +EOF +) +readonly usage + +if [[ "$#" -lt 2 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" +shift + +"$ci_dir/util/artifacts/stage.sh" "$artifact_name" "$@" > /dev/null +"$ci_dir/util/artifacts/upload_stage_packed.sh" "$artifact_name" diff --git a/cccl_upstream/ci/util/artifacts/upload_stage.sh b/cccl_upstream/ci/util/artifacts/upload_stage.sh new file mode 100755 index 00000000..ec778efc --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload_stage.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Same as 'ci/util/artifacts/upload_packed.sh', but assumes that the stage has already been created using +'ci/util/artifacts/stage.sh' and 'unstage.sh'. Performs the packing and registration steps only. +EOF +) +readonly usage + +if [[ "$#" -ne 1 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" +readonly artifact_dir="$ARTIFACT_UPLOAD_STAGE/${artifact_name}/${artifact_name}" + +start=$SECONDS +"$ci_dir/util/artifacts/upload/build.sh" "$artifact_name" +"$ci_dir/util/artifacts/upload/register.sh" "$artifact_name" "$artifact_dir" +echo "Artifact '$artifact_name' built in $((SECONDS - start)) seconds." diff --git a/cccl_upstream/ci/util/artifacts/upload_stage_packed.sh b/cccl_upstream/ci/util/artifacts/upload_stage_packed.sh new file mode 100755 index 00000000..d365e2bd --- /dev/null +++ b/cccl_upstream/ci/util/artifacts/upload_stage_packed.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/artifacts/common.sh +source "$ci_dir/util/artifacts/common.sh" + +usage=$(cat < + +Same as 'ci/util/artifacts/upload_packed.sh', but assumes that the stage has already been created using +'ci/util/artifacts/stage.sh' and 'unstage.sh'. Performs the packing and registration steps only. +EOF +) +readonly usage + +if [[ "$#" -ne 1 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +readonly artifact_name="$1" +# shellcheck disable=SC2154 +readonly artifact_archive="$ARTIFACT_UPLOAD_STAGE/${artifact_name}/${artifact_name}.tar.zst" + +start=$SECONDS +"$ci_dir/util/artifacts/upload/pack.sh" "$artifact_name" +"$ci_dir/util/artifacts/upload/register.sh" "$artifact_name" "$artifact_archive" +# Already compressed while packing: +"$ci_dir/util/artifacts/upload/set_compression_level.sh" "$artifact_name" 0 > /dev/null +echo "Artifact '$artifact_name' packed in $((SECONDS - start)) seconds." diff --git a/cccl_upstream/ci/util/build_and_test_targets.sh b/cccl_upstream/ci/util/build_and_test_targets.sh new file mode 100755 index 00000000..7714b8e8 --- /dev/null +++ b/cccl_upstream/ci/util/build_and_test_targets.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This must be run from the cccl repo root, but the script may be relocated by git_bisect.sh. +# Check that the current directory looks like the repo root: +if [[ ! -f "./cccl-version.json" ]]; then + echo "This script must be run from the cccl repo root." + exit 1 +fi + +usage() { + cat <&2; usage; exit 2 ;; + esac +done + +if [[ -z "${PRESET}" && -z "${CONFIGURE_OVERRIDE}" ]]; then + echo "::error:: --preset or --configure-override is required" >&2 + usage + exit 2 +fi + +if [[ -n "${CONFIGURE_OVERRIDE}" ]]; then + if [[ -n "${PRESET}" ]]; then + echo "::warning:: --preset ignored due to --configure-override" >&2 + fi + if [[ "${#CMAKE_OPTIONS[@]}" -gt 0 ]]; then + echo "::warning:: --cmake-options ignored due to --configure-override" >&2 + fi +fi + +echo "::group::⚙️ Testing $(git log --oneline | head -n1)" + +# Configure and parse the build directory from CMake output +BUILD_DIR="" +cmlog_file="$(mktemp /tmp/cmake-config-XXXXXX.log)" +if [[ -n "${CONFIGURE_OVERRIDE}" ]]; then + if ! (set -x; eval "${CONFIGURE_OVERRIDE}") 2>&1 | tee "${cmlog_file}"; then + echo "::endgroup::" + echo -e "🔴📝 Configuration override failed ($(elapsed_time)):\n\t${CONFIGURE_OVERRIDE}" + exit 1 + fi +else + if ! (set -x; cmake --preset "${PRESET}" "${CMAKE_OPTIONS[@]}") 2>&1 | tee "${cmlog_file}"; then + echo "::endgroup::" + echo "🔴📝 CMake configure failed for preset ${PRESET} ($(elapsed_time))" + exit 1 + fi +fi +BUILD_DIR=$(awk -F': ' '/-- Build files have been written to:/ {print $2}' "${cmlog_file}" | tail -n1) +if [[ -z "${BUILD_DIR}" ]]; then + echo "::endgroup::" + echo "🔴‼️ Unable to determine build directory ($(elapsed_time))" + exit 1 +fi + +if [[ "${#BUILD_TARGETS[@]}" -gt 0 ]]; then + if ! (set -x; ninja -C "${BUILD_DIR}" "${BUILD_TARGETS[@]}"); then + echo "::endgroup::" + echo "🔴🛠️ Ninja build failed for targets ($(elapsed_time)): ${BUILD_TARGETS[*]@Q}" + exit 1 + fi +fi + +if [[ "${#CTEST_TARGETS[@]}" -gt 0 ]]; then + for t in "${CTEST_TARGETS[@]}"; do + if ! (set -x; ctest --test-dir "${BUILD_DIR}" -R "$t" -V --output-on-failure); then + echo "::endgroup::" + echo "🔴🔎 CTest failed for target $t ($(elapsed_time))" + exit 1 + fi + done +fi + +if [[ "${#LIT_PRECOMPILE_TESTS[@]}" -gt 0 || "${#LIT_TESTS[@]}" -gt 0 ]]; then + lit_site_cfg="${BUILD_DIR}/libcudacxx/test/libcudacxx/lit.site.cfg" + if [[ ! -f "${lit_site_cfg}" ]]; then + echo "::endgroup::" + echo "🔴🧪 LIT site config not found ($(elapsed_time)): ${lit_site_cfg}" + exit 1 + fi +fi + +if [[ "${#LIT_PRECOMPILE_TESTS[@]}" -gt 0 ]]; then + for t in "${LIT_PRECOMPILE_TESTS[@]}"; do + t_path="libcudacxx/test/libcudacxx/${t}" + if ! (set -x; LIBCUDACXX_SITE_CONFIG="${lit_site_cfg}" lit -v "-Dexecutor=NoopExecutor()" "${t_path}"); then + echo "::endgroup::" + echo "🔴🧪 LIT precompile failed ($(elapsed_time)): ${t}" + exit 1 + fi + done +fi + +if [[ "${#LIT_TESTS[@]}" -gt 0 ]]; then + for t in "${LIT_TESTS[@]}"; do + t_path="libcudacxx/test/libcudacxx/${t}" + if ! (set -x; LIBCUDACXX_SITE_CONFIG="${lit_site_cfg}" lit -v "${t_path}"); then + echo "::endgroup::" + echo "🔴🧪 LIT test failed ($(elapsed_time)): ${t}" + exit 1 + fi + done +fi + +if [[ -n "${CUSTOM_TEST_CMD}" ]]; then + if ! (set -x; eval "${CUSTOM_TEST_CMD}"); then + echo "::endgroup::" + echo "🔴🧪 Custom test command failed ($(elapsed_time)): ${CUSTOM_TEST_CMD}" + exit 1 + fi +fi + +echo "::endgroup::" +echo "🟢✅ Passed ($(elapsed_time))" +exit 0 diff --git a/cccl_upstream/ci/util/create_mock_job_env.sh b/cccl_upstream/ci/util/create_mock_job_env.sh new file mode 100755 index 00000000..067966a7 --- /dev/null +++ b/cccl_upstream/ci/util/create_mock_job_env.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage=$(cat < + +Allows the scripts in ci/util/workflow and ci/util/artifacts to run as though they are running in a CI environment. + +Create a mock job environment for testing purposes. + +The run id can be found in the workflow run URL, and the job_id can be found at the start of the Run Command job step. + +A new shell is spawned with the remote environment of a specific job from a specific workflow run. + +Environment variables are configured to mimic the CI environment. + +Caches and previously downloaded artifacts in /tmp are deleted to ensure a clean state. + !! Note that this does affect the caller's filesystem: + /tmp/workflow + /tmp/ + and similar caches will be deleted **from the caller's filesystem**. + + This is usually fine, but be might overwrite files in-use by other mock environments. +EOF +) +readonly usage + +if [[ "$#" -ne 2 ]]; then + echo "Error: Invalid number of arguments." >&2 + echo "$usage" >&2 + exit 1 +fi + +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "$0: Detected another GITHUB_ACTIONS environment." >&2 + echo "unset GITHUB_ACTIONS if this is intentional." >&2 + exit 1 +fi + +if [[ -z "${DEVCONTAINER_NAME:-}" ]]; then + echo "This script must be run inside a devcontainer." >&2 + exit 1 +else + echo "Running in devcontainer: $DEVCONTAINER_NAME" +fi + +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" && pwd) + +export GITHUB_ACTIONS=true +export GITHUB_RUN_ID="$1" +export JOB_ID="$2" + +( + # shellcheck source=ci/util/workflow/common.sh + source "$ci_dir/util/workflow/common.sh" + # shellcheck source=ci/util/artifacts/common.sh + source "$ci_dir/util/artifacts/common.sh" + + rm -rf "$WORKFLOW_DIR" + rm -rf "$ARTIFACT_ARCHIVES" + rm -rf "$ARTIFACT_UPLOAD_STAGE" + rm -rf "$ARTIFACT_UPLOAD_REGISTERY" +) + +# Configure shell prompt: +export PS0="" +export PS1=" [\u@\h \W]$ " +export PROMPT_COMMAND="" + + +echo "Starting new shell for emulating Job $JOB_ID in Run $GITHUB_RUN_ID". +echo "" + +bash --norc --noprofile -i || : + +echo +echo "Exiting mock job environment." diff --git a/cccl_upstream/ci/util/extract_switches.sh b/cccl_upstream/ci/util/extract_switches.sh new file mode 100755 index 00000000..b8217534 --- /dev/null +++ b/cccl_upstream/ci/util/extract_switches.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Similar to getopt, but only extracts recognized switches and leaves all other arguments in place. +# +# Example Usage: +# new_args=$(extract_switches.sh -cpu-only -gpu-only -- "$@") +# declare -a new_args="(${new_args})" +# set -- "${new_args[@]}" +# while true; do +# case "$1" in +# -cpu-only) CPU_ONLY=true; shift;; +# -gpu-only) GPU_ONLY=true; shift;; +# --) shift; break;; +# *) echo "Unknown argument: $1"; exit 1;; +# esac +# done +# +# This leaves all unrecognized arguments in $@ for later parsing. + +# Parse switches +switches=() +for arg in "$@"; do + case "$arg" in + --help | -h) + cat <<"EOF" | cut -c 5- + Usage: extract_switches.sh [ ...] -- + + Sorts any recognized switches in argv to the front and returns the result. + Unrecognized switches are left in place. + + Example Usage: + new_args="$(extract_switches.sh -cpu-only -gpu-only -- "$@")" + declare -a new_args="(${new_args})" + set -- "${new_args[@]}" + while true; do + case "$1" in + -cpu-only) CPU_ONLY=true; shift;; + -gpu-only) GPU_ONLY=true; shift;; + --) shift; break;; + *) echo "Unknown argument: $1"; exit 1;; + esac + done +EOF + exit + ;; + --) + shift + break + ;; + *) + switches+=("$arg") + shift + ;; + esac +done + +found_switches=() +other_args=() +for arg in "$@"; do + for switch in "${switches[@]}"; do + if [[ "$arg" = "$switch" ]]; then + found_switches+=("\"$arg\"") + continue 2 + fi + done + other_args+=("\"$arg\"") +done + +echo "${found_switches[*]} -- ${other_args[*]}" diff --git a/cccl_upstream/ci/util/git_bisect.sh b/cccl_upstream/ci/util/git_bisect.sh new file mode 100755 index 00000000..53120639 --- /dev/null +++ b/cccl_upstream/ci/util/git_bisect.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ci_dir/.." + +usage() { + cat <&2; usage; exit 2 ;; + esac +done + +if [[ -z "${PRESET}" && -z "${CONFIGURE_OVERRIDE}" ]]; then + echo "::error:: --preset or --configure-override is required" >&2 + usage + exit 2 +fi + +if [[ -n "${CONFIGURE_OVERRIDE}" ]]; then + if [[ -n "${PRESET}" ]]; then + echo "::warning:: --preset ignored due to --configure-override" >&2 + fi + if [[ -n "${CMAKE_OPTIONS}" ]]; then + echo "::warning:: --cmake-options ignored due to --configure-override" >&2 + fi +fi + +# Ensure the checkout has complete history and tags: +git fetch --unshallow > /dev/null 2>&1 || : +git fetch --tags > /dev/null 2>&1 || : + +# Resolve good and bad refs +good_ref="${GOOD_REF}" +bad_ref="${BAD_REF}" + +# Helper to resolve '-Nd' (N days ago on origin/main) to a SHA +_resolve_days_ago() { + local spec="$1" + local base_branch="origin/main" + local n="${spec#-}" + n="${n%d}" + if [[ -z "$n" || ! "$n" =~ ^[0-9]+$ ]]; then + return 1 + fi + local when + when=$(date -u -d "$n days ago" '+%Y-%m-%d %H:%M:%S %z') + git rev-list -n 1 --before="$when" "$base_branch" +} + +# Resolve good_ref +if [[ -z "$good_ref" ]]; then + good_ref=$(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || :) + echo "Good ref defaulted to last release: $good_ref" +fi +if [[ "$good_ref" =~ ^-[0-9]+d$ ]]; then + good_sha=$(_resolve_days_ago "$good_ref") + if [[ -z "$good_sha" ]]; then + echo "::error::Unable to resolve good_ref '$good_ref' to a commit on origin/main" >&2 + exit 1 + fi + echo "Resolved good_ref '$good_ref' to origin/main @ $good_sha" +else + if [[ -z "$good_ref" ]]; then + echo "::error::Unable to determine good ref" >&2 + exit 1 + fi + good_sha=$(git rev-parse "$good_ref") +fi + +# Resolve bad_ref +if [[ -z "$bad_ref" ]]; then + bad_ref="origin/main" + echo "Bad ref defaulted to origin/main: $bad_ref" +fi +if [[ "$bad_ref" =~ ^-[0-9]+d$ ]]; then + bad_sha=$(_resolve_days_ago "$bad_ref") + if [[ -z "$bad_sha" ]]; then + echo "::error::Unable to resolve bad_ref '$bad_ref' to a commit on origin/main" >&2 + exit 1 + fi + echo "Resolved bad_ref '$bad_ref' to origin/main @ $bad_sha" +else + bad_sha=$(git rev-parse "$bad_ref") +fi + +# Copy the build-and-test runner to a temp file so it remains available as HEAD changes: +tmp_runner="$(mktemp /tmp/build-and-test-XXXXXX.sh)" +cp "${ci_dir}/util/build_and_test_targets.sh" "${tmp_runner}" +chmod +x "${tmp_runner}" + +# If --repeat > 1, wrap the runner to repeat successful runs to detect flakiness. +bisect_runner="${tmp_runner}" +if [[ "${REPEAT}" =~ ^[0-9]+$ ]] && [[ "${REPEAT}" -gt 1 ]]; then + tmp_repeat="$(mktemp /tmp/build-and-test-repeat-XXXXXX.sh)" + cat > "${tmp_repeat}" < 2) + | [{(.[]) : {"type": "header"}}] + | add + | $manifest + { "files" : . } +EOF + +find "$path" -wholename '*include/*' -type f -printf '%P\n' \ + | jq -s --raw-input --argjson manifest "$manifest" "$prog" > "${outfile}" diff --git a/cccl_upstream/ci/util/memmon.sh b/cccl_upstream/ci/util/memmon.sh new file mode 100755 index 00000000..63593af5 --- /dev/null +++ b/cccl_upstream/ci/util/memmon.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +set -euo pipefail + +pid_file="/tmp/.memmon.pid" + +log_threshold="2" +print_threshold="5" +poll_interval="5" +log_file="$PWD/memmon.log" +mode="" + +usage() { + cat <<'USAGE' +Monitors running processes for high memory usage, logging and reporting peaks above specified thresholds. + +Usage: memmon.sh (--start | --stop | --monitor | --help) + [--log-threshold ] # Write to log file if process exceeds this memory (default 2 GB) + [--print-threshold ] # Print to stdout if process exceeds this memory (default 5 GB) + [--poll ] # Poll interval for checking processes (default 5 seconds) + [--log-file ] # Log file path (default ./memmon.log) + +Modes: + + --start Start monitoring in the background (writes pid to /tmp/.memmon.pid) + --stop Stop monitoring (kills pid in /tmp/.memmon.pid) + --monitor Run monitoring in the foreground (for testing/debugging) + +Example Session: + + memmon.sh --start # Start monitoring + launch_memory_intensive_processes # Do work + memmon.sh --stop # Stop monitoring and write log + cat memmon.log # View log +USAGE +} + +error() { + echo "memmon: $*" >&2 + exit 1 +} + +error_usage() { + echo "memmon: $*" >&2 + usage + exit 1 +} + +ensure_absolute_log() { + case "$log_file" in + /*) return ;; + *) + local dir + dir="$(cd "$(dirname "$log_file")" && pwd)" + local base + base="$(basename "$log_file")" + log_file="$dir/$base" + ;; + esac +} + +to_kib() { + awk -v gib="$1" 'BEGIN {printf "%.0f", gib * 1024 * 1024}' +} + +format_gib() { + awk -v rss="$1" 'BEGIN {printf "%.3f", rss/1024/1024}' +} + +format_threshold() { + awk -v val="$1" 'BEGIN {printf "%.3f", val + 0}' +} + +declare -A MEMMON_MAX_RSS +declare -A MEMMON_CMD +declare -A MEMMON_TARGET + +get_cmdline() { + local pid="$1" + local cmdline_file="/proc/$pid/cmdline" + local raw + + # Attempt to read from /proc first for accuracy + if [[ -r "$cmdline_file" ]]; then + raw="$(tr '\0' ' ' <"$cmdline_file" 2>/dev/null)" + # Clean up whitespace + raw="${raw//$'\n'/ }" + raw="${raw//$'\r'/ }" + raw="${raw//$'\t'/ }" + while [[ "$raw" == *' ' ]]; do + raw="${raw% }" + done + if [[ -n "$raw" ]]; then + printf '%s' "$raw" + return 0 + fi + fi + # Fallback to ps if /proc is unavailable + raw="$(ps -wwp "$pid" -o command= 2>/dev/null | head -n1)" + # Clean up whitespace + raw="${raw//$'\n'/ }" + raw="${raw//$'\r'/ }" + raw="${raw//$'\t'/ }" + if [[ -n "$raw" ]]; then + printf '%s' "$raw" + return 0 + fi + printf '[command unavailable]' +} + +extract_target() { + local cmd="$1" + # Try to locate the name of the cmake target: + if [[ "$cmd" =~ CMakeFiles/([^[:space:]]*)\.dir ]]; then + printf '%s' "${BASH_REMATCH[1]}" + else + printf '-' + fi +} + +start_memmon() { + # Check if already running + if [[ -f "$pid_file" ]]; then + local existing_pid + existing_pid="$(<"$pid_file")" + if kill -0 "$existing_pid" 2>/dev/null; then + error "already running (pid $existing_pid)" + fi + rm -f "$pid_file" + fi + + ensure_absolute_log + + # Start monitoring in the background + "$0" --monitor \ + --log-threshold "$log_threshold" \ + --print-threshold "$print_threshold" \ + --poll "$poll_interval" \ + --log-file "$log_file" & + local child_pid=$! + echo "$child_pid" >"$pid_file" + echo "memmon started (pid $child_pid, log-threshold ${log_threshold}GB, print-threshold ${print_threshold}GB, log $log_file)" +} + +stop_memmon() { + if [[ ! -f "$pid_file" ]]; then + error "not running" + fi + local running_pid + running_pid="$(<"$pid_file")" + if ! kill -0 "$running_pid" 2>/dev/null; then + rm -f "$pid_file" + error "not running" + fi + + # Attempt graceful shutdown + kill "$running_pid" 2>/dev/null || true + + for _ in {1..20}; do + if ! kill -0 "$running_pid" 2>/dev/null; then + break + fi + sleep 0.25 + done + + # Force kill if still running + if kill -0 "$running_pid" 2>/dev/null; then + kill -9 "$running_pid" 2>/dev/null || true + fi + + # Wait for pid file to be removed + for _ in {1..20}; do + [[ ! -f "$pid_file" ]] && break + sleep 0.25 + done + [[ -f "$pid_file" ]] && rm -f "$pid_file" + echo "memmon stopped" +} + +monitor_mem() { + MEMMON_MAX_RSS=() + MEMMON_CMD=() + MEMMON_TARGET=() + + ensure_absolute_log + + local log_threshold_kib + log_threshold_kib="$(to_kib "$log_threshold")" + local print_threshold_kib + print_threshold_kib="$(to_kib "$print_threshold")" + + local running=true + + cleanup() { + trap - INT TERM EXIT + mkdir -p "$(dirname "$log_file")" + { + printf "peak-mem | PID | target | command-line\n" + if [[ ${#MEMMON_MAX_RSS[@]} -eq 0 ]]; then + printf "No processes exceeded %s GB\n" "$(format_threshold "$log_threshold")" + else + local tmp + tmp="$(mktemp)" + for pid in "${!MEMMON_MAX_RSS[@]}"; do + printf "%s\t%s\t%s\t%s\n" "${MEMMON_MAX_RSS[$pid]}" "$pid" "${MEMMON_TARGET[$pid]}" "${MEMMON_CMD[$pid]}" >>"$tmp" + done + sort -nr -k1,1 "$tmp" | while IFS=$'\t' read -r peak pid target cmd; do + local mem_gib + mem_gib="$(format_gib "$peak")" + printf "%s GB | %s | %s | %s\n" "$mem_gib" "$pid" "$target" "$cmd" + done + rm -f "$tmp" + fi + } >"$log_file" + echo "memmon log written to $log_file" + rm -f "$pid_file" + } + + trap 'running=false' INT TERM + trap cleanup EXIT + + while $running; do + while read -r pid rss; do + [[ -z "$pid" || -z "$rss" ]] && continue + [[ "$pid" =~ ^[0-9]+$ ]] || continue + [[ "$rss" =~ ^[0-9]+$ ]] || continue + if (( rss >= log_threshold_kib )); then + local current=${MEMMON_MAX_RSS[$pid]:-0} + if (( rss > current )); then + MEMMON_MAX_RSS[$pid]=$rss + MEMMON_CMD[$pid]=$(get_cmdline "$pid") + MEMMON_TARGET[$pid]=$(extract_target "${MEMMON_CMD[$pid]}") + if (( rss >= print_threshold_kib )); then + local mem_gib + mem_gib="$(format_gib "$rss")" + printf 'memmon: %s GB | %s | %s | %s\n' "$mem_gib" "$pid" "${MEMMON_TARGET[$pid]}" "${MEMMON_CMD[$pid]}" + fi + fi + fi + done < <(ps -eo pid=,rss=) + + if ! sleep "$poll_interval"; then + break + fi + done +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --log-threshold) + [[ $# -lt 2 ]] && error "--log-threshold requires a value" + log_threshold="$2" + shift 2 + ;; + --print-threshold) + [[ $# -lt 2 ]] && error "--print-threshold requires a value" + print_threshold="$2" + shift 2 + ;; + --log-file) + [[ $# -lt 2 ]] && error "--log-file requires a value" + log_file="$2" + shift 2 + ;; + --poll) + [[ $# -lt 2 ]] && error "--poll requires a value" + poll_interval="$2" + shift 2 + ;; + --start) + [[ -n "$mode" ]] && error "Specify only one of --start or --stop" + mode="start" + shift + ;; + --stop) + [[ -n "$mode" ]] && error "Specify only one of --start or --stop" + mode="stop" + shift + ;; + --monitor) + mode="monitor" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + error_usage "Unknown option: $1" + ;; + esac +done + +if [[ -z "$mode" ]]; then + error_usage "Must specify one of --start or --stop or --monitor" +fi + +case "$mode" in + start) + start_memmon + ;; + stop) + stop_memmon + ;; + monitor) + monitor_mem + ;; + *) + error_usage "Unhandled mode: $mode" + ;; +esac diff --git a/cccl_upstream/ci/util/pre-commit/check_cub_test_macros.py b/cccl_upstream/ci/util/pre-commit/check_cub_test_macros.py new file mode 100755 index 00000000..b3a4cb33 --- /dev/null +++ b/cccl_upstream/ci/util/pre-commit/check_cub_test_macros.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re +import sys + +RAW_TEST_MACROS = ( + "C2H_TEST", + "C2H_TEST_LIST", + "C2H_TEST_WITH_FIXTURE", + "C2H_TEST_LIST_WITH_FIXTURE", + "TEST_CASE", + "TEST_CASE_METHOD", + "SCENARIO", + "SCENARIO_METHOD", + "TEMPLATE_TEST_CASE", + "TEMPLATE_TEST_CASE_SIG", + "TEMPLATE_TEST_CASE_METHOD", + "TEMPLATE_TEST_CASE_METHOD_SIG", + "TEMPLATE_PRODUCT_TEST_CASE", + "TEMPLATE_PRODUCT_TEST_CASE_SIG", + "TEMPLATE_PRODUCT_TEST_CASE_METHOD", + "TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG", + "TEMPLATE_LIST_TEST_CASE", + "TEMPLATE_LIST_TEST_CASE_METHOD", +) + +RAW_TEST_MACRO_RE = re.compile( + r"^[ \t]*(?P" + + "|".join( + re.escape(macro) for macro in sorted(RAW_TEST_MACROS, key=len, reverse=True) + ) + + r")[ \t]*\(", + re.MULTILINE, +) + + +def remove_comments(source: str) -> str: + """Blank C++ comments while preserving line and column positions.""" + result = list(source) + index = 0 + + while index < len(source): + if source.startswith("//", index): + end = source.find("\n", index) + if end == -1: + end = len(source) + for comment_index in range(index, end): + result[comment_index] = " " + index = end + continue + + if source.startswith("/*", index): + end = source.find("*/", index + 2) + if end == -1: + end = len(source) - 2 + for comment_index in range(index, min(end + 2, len(source))): + if source[comment_index] not in "\r\n": + result[comment_index] = " " + index = end + 2 + continue + + if source[index] in {'"', "'"}: + quote = source[index] + index += 1 + while index < len(source): + if source[index] == "\\": + index += 2 + continue + if source[index] == quote: + index += 1 + break + index += 1 + continue + + index += 1 + + return "".join(result) + + +def self_test() -> bool: + fixtures = [ + ('// C2H_TEST("x")', False), + ('/*\nTEST_CASE("x")\n*/', False), + ('const char* value = "TEST_CASE(";', False), + ('CUB_TEST("x", "[y]", CUB_SMALL)', False), + ('CUB_TEST_CASE("x", "[y]", CUB_LARGE)', False), + ('CUB_TEST_LIST("x", "[y]", CUB_SMALL, types)', False), + ] + fixtures.extend((f'{macro}("x")', True) for macro in RAW_TEST_MACROS) + + for source, expected in fixtures: + found = bool(RAW_TEST_MACRO_RE.search(remove_comments(source))) + if found != expected: + expected_result = "match" if expected else "no match" + actual_result = "match" if found else "no match" + print( + "internal error: test-registration checker self-test failed for " + f"{source!r}: expected {expected_result}, found {actual_result}. " + "This is a problem with the checker, not the files being committed.", + file=sys.stderr, + ) + return False + + return True + + +def check_file(filename: str) -> bool: + with open(filename, encoding="utf-8", errors="surrogateescape") as source_file: + source = source_file.read() + + source_without_comments = remove_comments(source) + found_error = False + for match in RAW_TEST_MACRO_RE.finditer(source_without_comments): + line = source.count("\n", 0, match.start()) + 1 + column = match.start("macro") - source.rfind("\n", 0, match.start("macro")) + print( + f"{filename}:{line}:{column}: {match.group('macro')} bypasses CUB " + "memory classification; use CUB_TEST, CUB_TEST_CASE, or CUB_TEST_LIST." + ) + found_error = True + + return found_error + + +def main() -> int: + if not self_test(): + return 2 + + found_error = False + for filename in sys.argv[1:]: + found_error = check_file(filename) or found_error + return int(found_error) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cccl_upstream/ci/util/pre-commit/check_shebang.py b/cccl_upstream/ci/util/pre-commit/check_shebang.py new file mode 100755 index 00000000..a00e4a0f --- /dev/null +++ b/cccl_upstream/ci/util/pre-commit/check_shebang.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +import re +import sys + + +def main() -> int: + # We pre-compile this regular expression as a micro-optimization for speed. The + # assumption is that the shebang is correct, so everything in the happy path should be + # as fast as possible. + FIRST_LINE_RE = re.compile(r"#!\s*/usr/bin/env.*") + ret = 0 + for f in sys.argv[1:]: + with open(f) as fd: + first_line = fd.readline() + + if not first_line.startswith("#!"): + # Not a shebang + continue + + if FIRST_LINE_RE.match(first_line): + # Already correct + continue + + ret = 1 + + if not ( + m := re.match(r"#!\s*(?:/bin/(\w+)|/usr/bin/(\w+))\s*(.*)", first_line) + ): + # Not assert, pre-commit may compile with -O + raise AssertionError(f"Failed to match shebang for {first_line}") + + fixed = f"#!/usr/bin/env {m[1] or m[2]}".rstrip() + if rest := m[3].strip(): + fixed += f" {rest}" + fixed += "\n" + + with open(f) as fd: + # Read the remaining lines, we need them in order to overwrite + lines = fd.readlines() + + lines[0] = fixed + + with open(f, "w") as fd: + fd.writelines(lines) + + return ret + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cccl_upstream/ci/util/pre-commit/strip_unprintable.py b/cccl_upstream/ci/util/pre-commit/strip_unprintable.py new file mode 100755 index 00000000..6b6ec013 --- /dev/null +++ b/cccl_upstream/ci/util/pre-commit/strip_unprintable.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# strip_unprintable.py - Remove invisible / unprintable characters from text files. +import re +import sys +from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter + +# Canonical definition of what gets removed. One row per range group: +# (regex character-class fragment, human description) +# The compiled character class and the --help listing are both derived from this +# table, so adding or removing a range only needs to happen here. TAB (U+0009), +# LF (U+000A), and CR (U+000D) are deliberately excluded from the C0 range. The +# fragments use \x/\u escapes so the source file itself stays free of the very +# characters this script removes. +RANGES = ( + ( + r"\x00-\x08\x0b\x0c\x0e-\x1f\x7f", + "C0 controls / DEL (TAB, LF, CR preserved)", + ), + (r"\x80-\x9f", "C1 controls"), + (r"\xa0", "no-break space"), + (r"\u200b-\u200f", "zero-width space/joiners, bidi marks"), + (r"\u202a-\u202e", "bidi embedding/override"), + (r"\u2060-\u2064", "word joiner, invisible operators"), + (r"\ufeff", "BOM / zero-width no-break space"), +) + +# Character class assembled from column 1 of the ranges table. +BAD_RE = re.compile("[" + "".join(frag for frag, _ in RANGES) + "]") + + +def parse_args() -> Namespace: + removed = "\n".join(f" {frag}\t{desc}" for frag, desc in RANGES) + parser = ArgumentParser( + description=( + "Remove invisible / unprintable characters from text files, in place, " + "while preserving ordinary whitespace (TAB U+0009, LF U+000A, " + "CR U+000D)." + ), + epilog=f"Removed characters:\n{removed}", + formatter_class=RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--check", + action="store_true", + help=( + "Report offending files and their line/column locations; make no " + "edits. Exits non-zero if any unprintable characters are found." + ), + ) + parser.add_argument("files", nargs="+", metavar="FILE") + return parser.parse_args() + + +def check(files: list[str]) -> int: + ret = 0 + for f in files: + with open(f, encoding="utf-8", errors="surrogateescape") as fd: + for lineno, line in enumerate(fd, start=1): + for m in BAD_RE.finditer(line): + print(f"{f}:{lineno}:{m.start() + 1}: U+{ord(m.group()):04X}") + ret = 1 + + return ret + + +def strip(files: list[str]) -> int: + ret = 0 + for f in files: + with open(f, encoding="utf-8", errors="surrogateescape") as fd: + original = fd.read() + stripped = BAD_RE.sub("", original) + if stripped != original: + with open( + f, "w", encoding="utf-8", errors="surrogateescape", newline="" + ) as fd: + fd.write(stripped) + ret = 1 + + return ret + + +def main() -> int: + args = parse_args() + if args.check: + return check(args.files) + return strip(args.files) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cccl_upstream/ci/util/python/common_arg_parser.sh b/cccl_upstream/ci/util/python/common_arg_parser.sh new file mode 100644 index 00000000..c7fd41e7 --- /dev/null +++ b/cccl_upstream/ci/util/python/common_arg_parser.sh @@ -0,0 +1,58 @@ +# Argument parser for Python CI scripts. +parse_python_args() { + # Initialize variables + py_version="" + # ctk_mode carries the -ctk-mode value; empty means the default ("pinned"). + ctk_mode="" + + while [[ $# -gt 0 ]]; do + case $1 in + -py-version=*) + py_version="${1#*=}" + shift + ;; + -py-version) + if [[ $# -lt 2 ]]; then + echo "Error: -py-version requires a value" >&2 + return 1 + fi + py_version="$2" + shift 2 + ;; + -ctk-mode=*) + ctk_mode="${1#*=}" + # Reject an explicit-but-empty value (e.g. `-ctk-mode=`): a lane + # that wants the default omits the flag entirely, so an empty + # value signals a malformed generated argument -- fail loudly. + if [[ -z "${ctk_mode}" ]]; then + echo "Error: -ctk-mode requires a value" >&2 + return 1 + fi + shift + ;; + -ctk-mode) + if [[ $# -lt 2 || -z "$2" ]]; then + echo "Error: -ctk-mode requires a value" >&2 + return 1 + fi + ctk_mode="$2" + shift 2 + ;; + *) + # Unknown argument, ignore + shift + ;; + esac + done + + # Export for use by the calling script (py_version and ctk_mode are its inputs). + export py_version ctk_mode +} + +require_py_version() { + if [[ -z "$py_version" ]]; then + echo "Error: -py-version is required" >&2 + [[ -n "$1" ]] && echo "$1" >&2 + return 1 + fi +} diff --git a/cccl_upstream/ci/util/retry.sh b/cccl_upstream/ci/util/retry.sh new file mode 100755 index 00000000..171fd9e3 --- /dev/null +++ b/cccl_upstream/ci/util/retry.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +if [[ "$#" -lt 3 ]]; then + echo "Usage: $0 num_tries sleep_time command [args...]" + echo " num_tries: Number of attempts to run the command" + echo " sleep_time: Time to wait between attempts (in seconds)" + echo " command: The command to run" + echo " args: Arguments to pass to the command" + exit 1 +fi + +num_tries=$1 +sleep_time=$2 +shift 2 +command=("${*@Q}") + +# Loop until the command succeeds or we reach the maximum number of attempts: +for ((i=1; i<=num_tries; i++)); do + echo "Attempt ${i} of ${num_tries}: Running command '${command[*]}'" + status=0 + eval "${command[*]}" || status=$? + + if [[ "$status" -eq 0 ]]; then + echo "Command '${command[*]}' succeeded on attempt ${i}." + exit 0 + else + echo "Command '${command[*]}' failed with status ${status}. Retrying in ${sleep_time} seconds..." + sleep "$sleep_time" + fi +done +echo "Command '${command[*]}' failed after ${num_tries} attempts." +exit 1 diff --git a/cccl_upstream/ci/util/version_compare.sh b/cccl_upstream/ci/util/version_compare.sh new file mode 100755 index 00000000..b60bf6b2 --- /dev/null +++ b/cccl_upstream/ci/util/version_compare.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage=$(cat < A.B[.C[.D[...]]] + +Compares two version strings with the specified operator. + +compare_ops: + lt - less than + le - less than or equal to + eq - equal to + ne - not equal to + ge - greater than or equal to + gt - greater than +EOF +) +readonly usage + +if [[ "$#" -ne 3 ]]; then + echo "Error: Invalid arguments: $*" >&2 + echo "$usage" >&2 + exit 1 +fi + +version_a="$1" +operator="$2" +version_b="$3" + +# Validate operator +if [[ ! "$operator" =~ ^(lt|le|eq|ne|ge|gt)$ ]]; then + echo "Error: Invalid operator '$operator'. Must be one of: lt, le, eq, ne, ge, gt." >&2 + echo "$usage" >&2 + exit 1 +fi + +# Validate versions: +version_regex='^[0-9]+(\.[0-9]+)*$' +if [[ ! "$version_a" =~ $version_regex ]]; then + echo "Error: Invalid version string '$version_a'." >&2 + echo "$usage" >&2 + exit 1 +fi +if [[ ! "$version_b" =~ $version_regex ]]; then + echo "Error: Invalid version string '$version_b'." >&2 + echo "$usage" >&2 + exit 1 +fi + +# Split versions into arrays +IFS='.' read -r -a ver_a_parts <<< "$version_a" +IFS='.' read -r -a ver_b_parts <<< "$version_b" +max_length=${#ver_a_parts[@]} +if [[ "${#ver_b_parts[@]}" -gt "$max_length" ]]; then + max_length=${#ver_b_parts[@]} +fi + +# Compare each part +for ((i=0; i part_b)); then + result="gt" + break + else + result="eq" + fi +done + +# Evaluate the comparison based on the operator +case "$operator" in + lt) [[ "$result" == "lt" ]] ;; + le) [[ "$result" == "lt" || "$result" == "eq" ]] ;; + eq) [[ "$result" == "eq" ]] ;; + ne) [[ "$result" != "eq" ]] ;; + ge) [[ "$result" == "gt" || "$result" == "eq" ]] ;; + gt) [[ "$result" == "gt" ]] ;; + *) echo "Error: Unhandled operator '${operator}'." >&2; exit 1 ;; +esac + +exit $? diff --git a/cccl_upstream/ci/util/workflow/common.sh b/cccl_upstream/ci/util/workflow/common.sh new file mode 100755 index 00000000..a5fa4de9 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/common.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "This script must be sourced, not executed directly." >&2 + exit 1 +fi + +if [[ -z "${GITHUB_ACTIONS:-}" ]]; then + echo "This script must be run in a GitHub Actions environment." >&2 + exit 1 +fi + +to_posix_path() { + local path="$1" + + if [[ "$path" =~ ^([A-Za-z]):([\\/]?.*)$ ]]; then + local drive="${BASH_REMATCH[1]}" + local rest="${BASH_REMATCH[2]}" + rest="${rest//\\/\/}" + printf '/%s%s\n' "${drive,,}" "$rest" + return + fi + + printf '%s\n' "$path" +} + +runner_temp_posix="$(to_posix_path "${RUNNER_TEMP:-/tmp}")" + +export WORKFLOW_ARTIFACT="workflow" +export WORKFLOW_DIR="${runner_temp_posix}/workflow" + +mkdir -p "$WORKFLOW_DIR" diff --git a/cccl_upstream/ci/util/workflow/get_consumers.sh b/cccl_upstream/ci/util/workflow/get_consumers.sh new file mode 100755 index 00000000..a4db72ff --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_consumers.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +consumers=$(jq --arg job_id "$job_id" ' + to_entries[] + | select(.value.two_stage) + | .value.two_stage[] + | select(any(.producers[]; .id == $job_id)) + | .consumers +' "$WORKFLOW_DIR/workflow.json") + +echo "$consumers" diff --git a/cccl_upstream/ci/util/workflow/get_job_def.sh b/cccl_upstream/ci/util/workflow/get_job_def.sh new file mode 100755 index 00000000..37df5311 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_job_def.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +job_obj=$(jq --arg job_id "$job_id" ' + to_entries[] + | .value + | ( + (select(has("standalone")) | .standalone[] | select(.id == $job_id)) // + (select(has("two_stage")) | .two_stage[] | .producers[] | select(.id == $job_id)) // + (select(has("two_stage")) | .two_stage[] | .consumers[] | select(.id == $job_id)) + ) +' "$WORKFLOW_DIR/workflow.json") + +if [[ -z "$job_obj" ]]; then + echo "Error: No job definition found for job ID '$job_id'." >&2 + exit 1 +fi + +echo "$job_obj" | jq -r diff --git a/cccl_upstream/ci/util/workflow/get_job_project.sh b/cccl_upstream/ci/util/workflow/get_job_project.sh new file mode 100755 index 00000000..ebbfe1ec --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_job_project.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +job_def=$("${ci_dir}/util/workflow/get_job_def.sh" "$job_id") +project=$(echo "$job_def" | jq -r '.origin.matrix_job.project') +echo "$project" diff --git a/cccl_upstream/ci/util/workflow/get_producer_id.sh b/cccl_upstream/ci/util/workflow/get_producer_id.sh new file mode 100755 index 00000000..5b6df658 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_producer_id.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +producers=$(jq --arg job_id "$job_id" ' + to_entries[] + | select(.value.two_stage) + | .value.two_stage[] + | select(any(.consumers[]; .id == $job_id)) + | .producers +' "$WORKFLOW_DIR/workflow.json") + +producer_count=$(echo "$producers" | jq 'length') +if [[ "$producer_count" -ne 1 ]]; then + echo "Error: Expected exactly one producer for job ID '$job_id', but found ${producer_count:-0}." >&2 + exit 1 +fi + +producer_id=$(echo "$producers" | jq -r '.[0].id') +if [[ -z "$producer_id" ]]; then + echo "Error: No producer ID found for job ID '$job_id'." >&2 + exit 1 +fi +echo "$producer_id" diff --git a/cccl_upstream/ci/util/workflow/get_producers.sh b/cccl_upstream/ci/util/workflow/get_producers.sh new file mode 100755 index 00000000..ec037421 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_producers.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +producers=$(jq --arg job_id "$job_id" ' + to_entries[] + | select(.value.two_stage) + | .value.two_stage[] + | select(any(.consumers[]; .id == $job_id)) + | .producers +' "$WORKFLOW_DIR/workflow.json") + +echo "$producers" diff --git a/cccl_upstream/ci/util/workflow/get_stable_job_hash.sh b/cccl_upstream/ci/util/workflow/get_stable_job_hash.sh new file mode 100755 index 00000000..7753f905 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_stable_job_hash.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +job_def=$("${ci_dir}/util/workflow/get_job_def.sh" "$job_id" | jq 'del(.id, .origin)') +job_hash=$(echo "$job_def" | sha256sum | awk '{print $1}') +echo "$job_hash" diff --git a/cccl_upstream/ci/util/workflow/get_wheel_artifact_name.sh b/cccl_upstream/ci/util/workflow/get_wheel_artifact_name.sh new file mode 100755 index 00000000..b1058c1e --- /dev/null +++ b/cccl_upstream/ci/util/workflow/get_wheel_artifact_name.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +job_def=$("${ci_dir}/util/workflow/get_job_def.sh" "$job_id") + +py_version=$(echo "$job_def" | jq -r '.origin.matrix_job.py_version') +host=$(echo "$job_def" | jq -r '.origin.matrix_job.cxx_family') +if [[ "$host" == "MSVC" ]]; then + os="windows" +else + os="linux" +fi +arch=$(echo "$job_def" | jq -r '.origin.matrix_job.cpu') +project=$(echo "$job_def" | jq -r '.origin.matrix_job.project') + +for tag in "$py_version" "$os" "$arch"; do + if [[ -z "$tag" ]]; then + echo "Error: Missing required field in job definition for job ID '$job_id'." >&2 + echo "$usage" >&2 + echo >&2 + "Job definition: $job_def" >&2 + exit 1 + fi +done + +# v1 and v2 Python build jobs both run in the same workflow, so their wheel +# artifacts must have distinct names or the second upload clobbers the first +# and downstream test jobs grab the wrong wheel. v1 keeps its historical name +# (the test-cpu-import workflow hardcodes it); v2 gets a "-v2" suffix. +suffix="" +if [[ "$project" == "python_v2" ]]; then + suffix="-v2" +elif [[ "$project" == "python_tsan" ]]; then + # ThreadSanitizer-instrumented wheel (free-threaded TSan nightly lane). Must + # be distinct so its build doesn't clobber the normal wheel and the TSan test + # job doesn't grab an uninstrumented one. + suffix="-tsan" +fi + +echo "wheel-cccl${suffix}-$os-$arch-py$py_version" diff --git a/cccl_upstream/ci/util/workflow/has_consumers.sh b/cccl_upstream/ci/util/workflow/has_consumers.sh new file mode 100755 index 00000000..7202ac82 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/has_consumers.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +matching_producer=$(jq --arg job_id "$job_id" ' + to_entries[] + | select(.value.two_stage) + | .value.two_stage[] + | .producers[] + | select(.id == $job_id) +' "$WORKFLOW_DIR/workflow.json") + +if [[ -n "$matching_producer" ]]; then + exit 0 +else + exit 1 +fi diff --git a/cccl_upstream/ci/util/workflow/has_producers.sh b/cccl_upstream/ci/util/workflow/has_producers.sh new file mode 100755 index 00000000..72ee3143 --- /dev/null +++ b/cccl_upstream/ci/util/workflow/has_producers.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +job_id="${1:-${JOB_ID:-}}" + +if [[ -z "$job_id" ]]; then + echo "Error: No job ID provided and \$JOB_ID is not set." >&2 + echo "$usage" >&2 + exit 1 +fi + +"${ci_dir}/util/workflow/initialize.sh" + +matching_consumer=$(jq --arg job_id "$job_id" ' + to_entries[] + | select(.value.two_stage) + | .value.two_stage[] + | .consumers[] + | select(.id == $job_id) +' "$WORKFLOW_DIR/workflow.json") + +if [[ -n "$matching_consumer" ]]; then + exit 0 +else + exit 1 +fi diff --git a/cccl_upstream/ci/util/workflow/initialize.sh b/cccl_upstream/ci/util/workflow/initialize.sh new file mode 100755 index 00000000..f4f13bbb --- /dev/null +++ b/cccl_upstream/ci/util/workflow/initialize.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ci_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)" +readonly ci_dir +# shellcheck source=ci/util/workflow/common.sh +source "$ci_dir/util/workflow/common.sh" + +usage=$(cat <&2 + echo "$usage" >&2 + exit 1 +fi + +if [[ ! -f "$WORKFLOW_DIR/workflow.json" ]]; then + "$ci_dir/util/artifacts/download/fetch.sh" "$WORKFLOW_ARTIFACT" "$WORKFLOW_DIR" > /dev/null +fi diff --git a/cccl_upstream/ci/verify_codegen_libcudacxx.sh b/cccl_upstream/ci/verify_codegen_libcudacxx.sh new file mode 100755 index 00000000..da7b999b --- /dev/null +++ b/cccl_upstream/ci/verify_codegen_libcudacxx.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -eo pipefail + +# Ensure the script is being executed in its containing directory +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +# TBD: verbose? any extra args? + +source ./pretty_printing.sh + +pushd .. > /dev/null +GROUP_NAME="🛠️ CMake Configure Libcudacxx Codegen" +run_command "$GROUP_NAME" cmake --preset libcudacxx-codegen +status=$? +popd > /dev/null + +pushd .. > /dev/null +GROUP_NAME="🏗️ Build Libcudacxx Codegen" +run_command "$GROUP_NAME" cmake --build --preset libcudacxx-codegen + +status=$? +popd > /dev/null + +pushd .. > /dev/null +GROUP_NAME="🚀 Test Libcudacxx Codegen" +run_command "$GROUP_NAME" ctest --preset libcudacxx-codegen +status=$? +popd > /dev/null diff --git a/cccl_upstream/ci/windows/build_cccl_c_parallel.ps1 b/cccl_upstream/ci/windows/build_cccl_c_parallel.ps1 new file mode 100644 index 00000000..19a83a55 --- /dev/null +++ b/cccl_upstream/ci/windows/build_cccl_c_parallel.ps1 @@ -0,0 +1,28 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Remove-Module -Name build_common -ErrorAction SilentlyContinue +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @(20, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cccl-c-parallel" +$LOCAL_CMAKE_OPTIONS = "" + +configure_and_build_preset "CCCL C Parallel" $PRESET $LOCAL_CMAKE_OPTIONS + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/build_cccl_c_parallel_v2.ps1 b/cccl_upstream/ci/windows/build_cccl_c_parallel_v2.ps1 new file mode 100644 index 00000000..529a37b1 --- /dev/null +++ b/cccl_upstream/ci/windows/build_cccl_c_parallel_v2.ps1 @@ -0,0 +1,28 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Remove-Module -Name build_common -ErrorAction SilentlyContinue +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @(20, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cccl-c-parallel-v2" +$LOCAL_CMAKE_OPTIONS = "" + +configure_and_build_preset "CCCL C Parallel" $PRESET $LOCAL_CMAKE_OPTIONS + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/build_common.psm1 b/cccl_upstream/ci/windows/build_common.psm1 new file mode 100644 index 00000000..7d6b9554 --- /dev/null +++ b/cccl_upstream/ci/windows/build_common.psm1 @@ -0,0 +1,233 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +# We need the full path to cl because otherwise cmake will replace CMAKE_CXX_COMPILER with the full path +# and keep CMAKE_CUDA_HOST_COMPILER at "cl" which breaks our cmake script +$script:HOST_COMPILER = (Get-Command "cl").source -replace '\\','/' +$script:PARALLEL_LEVEL = $env:NUMBER_OF_PROCESSORS + +Write-Host "=== Docker Container Resource Info ===" +Write-Host "Number of Processors: $script:PARALLEL_LEVEL" +Get-WmiObject Win32_OperatingSystem | ForEach-Object { + Write-Host ("Memory: total={0:N1} GB, free={1:N1} GB" -f ($_.TotalVisibleMemorySize / 1MB), ($_.FreePhysicalMemory / 1MB)) +} +Write-Host "======================================" + +# Extract the CL version for export to build scripts: +$script:CL_VERSION_STRING = & cl.exe /? +if ($script:CL_VERSION_STRING -match "Version (\d+\.\d+)\.\d+") { + $CL_VERSION = [version]$matches[1] + Write-Host "Detected cl.exe version: $CL_VERSION" +} + +$script:GLOBAL_CMAKE_OPTIONS = $CMAKE_OPTIONS +if ($CUDA_ARCH) { + $script:GLOBAL_CMAKE_OPTIONS += ' "-DCMAKE_CUDA_ARCHITECTURES={0}"' -f $CUDA_ARCH +} + +# Default to pedantic mode in CI (GitHub Actions) +if ($env:GITHUB_ACTIONS) { + $script:GLOBAL_CMAKE_OPTIONS += ' "-DCCCL_ENABLE_WERROR=ON" "-DCCCL_ENABLE_PRAGMA_SYSTEM_HEADER=OFF"' +} else { + $script:GLOBAL_CMAKE_OPTIONS += ' "-DCCCL_ENABLE_WERROR=OFF" "-DCCCL_ENABLE_PRAGMA_SYSTEM_HEADER=ON"' +} + +if (-not $env:CCCL_BUILD_INFIX) { + $env:CCCL_BUILD_INFIX = "" +} + +# Presets will be configured in this directory: +$BUILD_DIR = "../build/$env:CCCL_BUILD_INFIX" + +If(!(test-path -PathType container "../build")) { + New-Item -ItemType Directory -Path "../build" +} + +# The most recent build will always be symlinked to cccl/build/latest +New-Item -ItemType Directory -Path "$BUILD_DIR" -Force + +# Convert to an absolute path: +$BUILD_DIR = (Get-Item -Path "$BUILD_DIR").FullName + +# Prepare environment for CMake: +$env:CMAKE_BUILD_PARALLEL_LEVEL = $PARALLEL_LEVEL +$env:CTEST_PARALLEL_LEVEL = 1 +$env:CUDAHOSTCXX = $script:HOST_COMPILER +$env:CXX = $script:HOST_COMPILER + +Write-Host "========================================" +Write-Host "Begin build" +Write-Host "pwd=$pwd" +Write-Host "BUILD_DIR=$BUILD_DIR" +Write-Host "CXX_STANDARD=$CXX_STANDARD" +Write-Host "CXX=$env:CXX" +Write-Host "CUDACXX=$env:CUDACXX" +Write-Host "CUDAHOSTCXX=$env:CUDAHOSTCXX" +Write-Host "TBB_ROOT=$env:TBB_ROOT" +Write-Host "NVCC_VERSION=$NVCC_VERSION" +Write-Host "CMAKE_BUILD_PARALLEL_LEVEL=$env:CMAKE_BUILD_PARALLEL_LEVEL" +Write-Host "CTEST_PARALLEL_LEVEL=$env:CTEST_PARALLEL_LEVEL" +Write-Host "CCCL_BUILD_INFIX=$env:CCCL_BUILD_INFIX" +Write-Host "GLOBAL_CMAKE_OPTIONS=$script:GLOBAL_CMAKE_OPTIONS" +Write-Host "Current commit is:" +Write-Host "$(git log -1 --format=short)" +Write-Host "========================================" + +cmake --version +ctest --version + +function configure_preset { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$BUILD_NAME, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PRESET, + [Parameter(Mandatory = $false)] + [string]$LOCAL_CMAKE_OPTIONS = "" + ) + + $step = "$BUILD_NAME (configure)" + + # CMake must be invoked in the same directory as the presets file: + pushd ".." + + # Echo and execute command to stdout: + $configure_command = "cmake --preset $PRESET --log-level VERBOSE" + if ($LOCAL_CMAKE_OPTIONS) { + $configure_command += " $LOCAL_CMAKE_OPTIONS" + } + if ($script:GLOBAL_CMAKE_OPTIONS) { + $configure_command += " $script:GLOBAL_CMAKE_OPTIONS" + } + + Write-Host $configure_command + Invoke-Expression $configure_command + $test_result = $LastExitCode + + If ($test_result -ne 0) { + throw "$step Failed" + } + + popd + Write-Host "$step complete." +} + +function build_preset { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$BUILD_NAME, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PRESET + ) + + $step = "$BUILD_NAME (build)" + + # CMake must be invoked in the same directory as the presets file: + pushd ".." + + sccache -z >$null + + cmake --build --preset $PRESET -v + $test_result = $LastExitCode + + $preset_dir = "${BUILD_DIR}/${PRESET}" + $sccache_json = "${preset_dir}/sccache_stats.json" + + sccache --show-adv-stats + sccache --show-adv-stats --stats-format=json > "${sccache_json}" + + echo "$step complete" + + If ($test_result -ne 0) { + throw "$step Failed" + } + + popd +} + +function test_preset { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$BUILD_NAME, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PRESET + ) + + $step = "$BUILD_NAME (test)" + + # CTest must be invoked in the same directory as the presets file: + pushd ".." + + sccache -z >$null + + ctest --preset $PRESET + $test_result = $LastExitCode + + sccache --show-adv-stats + + echo "$step complete" + + If ($test_result -ne 0) { + throw "$step Failed" + } + + popd +} + +function configure_and_build_preset { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$BUILD_NAME, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PRESET, + [Parameter(Mandatory = $false)] + [string]$LOCAL_CMAKE_OPTIONS = "" + ) + + configure_preset $BUILD_NAME $PRESET $LOCAL_CMAKE_OPTIONS + build_preset $BUILD_NAME $PRESET +} + +function Invoke-Checked { + <# + .SYNOPSIS + Runs a script block and throws if the last native command in it exits + non-zero. $ErrorActionPreference = "Stop" does not make native commands + (python/pip/pytest/...) throw, so their $LASTEXITCODE must be checked + explicitly; this wraps that boilerplate into one call. + .EXAMPLE + Invoke-Checked { & $python -m pip install pytest } "pip install failed" + #> + param( + [Parameter(Mandatory, Position = 0)][scriptblock]$ScriptBlock, + [Parameter(Position = 1)][string]$ErrorMessage = "Native command failed" + ) + & $ScriptBlock + if ($LASTEXITCODE -ne 0) { + throw "$ErrorMessage (exit code $LASTEXITCODE)" + } +} + +Export-ModuleMember -Function configure_preset, build_preset, test_preset, configure_and_build_preset, Invoke-Checked +Export-ModuleMember -Variable BUILD_DIR, CL_VERSION diff --git a/cccl_upstream/ci/windows/build_common_python.psm1 b/cccl_upstream/ci/windows/build_common_python.psm1 new file mode 100644 index 00000000..ba778236 --- /dev/null +++ b/cccl_upstream/ci/windows/build_common_python.psm1 @@ -0,0 +1,246 @@ +function Get-Python { + <# + .SYNOPSIS + Returns the path of the Python interpreter satisfying the supplied + version, installing it via uv if necessary. + .PARAMETER Version + A string in the form 'M.m' (e.g., '3.10', '3.13') or a free-threaded + version such as '3.14t'. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [ValidatePattern('^\d+\.\d+t?$')] + [string]$Version + ) + + # Install uv if not present. uv downloads pre-built CPython binaries -- + # no compilation, no build dependencies, no pyenv-win required. + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Host "Installing uv..." + Invoke-RestMethod https://astral.sh/uv/install.ps1 | Invoke-Expression + # uv installs to $HOME\.local\bin on Windows + $uvBin = Join-Path $HOME '.local\bin' + $Env:PATH = $uvBin + ";" + $Env:PATH + } + + Write-Host "Creating Python $Version venv via uv..." + $venvDir = Join-Path $HOME '.cccl-venv' + & uv venv --seed --python $Version $venvDir + if ($LASTEXITCODE -ne 0) { + throw [System.InvalidOperationException]::new( + "Failed to create Python $Version venv via uv." + ) + } + + $exe = Join-Path $venvDir 'Scripts\python.exe' + if (-not (Test-Path $exe)) { + throw [System.InvalidOperationException]::new( + "Could not find python.exe in venv at $exe" + ) + } + + Write-Host "Python $Version at: $exe" + + # Add venv Scripts dir to PATH so bare `python` and `pip` work. + $scriptsDir = Join-Path $venvDir 'Scripts' + $Env:PATH = $scriptsDir + ";" + $Env:PATH + + return $exe +} + +function Get-RepoRoot { + return (Resolve-Path "$PSScriptRoot/../..") +} + +function Get-CudaMajor { + <# + .SYNOPSIS + Gets the CUDA major version for this container instance (e.g. '12' or + '13'). Defaults to '13' if no match can be found. + #> + if ($env:CUDA_PATH) { + $nvcc = Join-Path $env:CUDA_PATH "bin/nvcc.exe" + if (Test-Path $nvcc) { + $out = & $nvcc --version 2>&1 + $text = ($out -join "`n") + if ($text -match 'release\s+(\d+)\.') { return $Matches[1] } + } + # Fallback: parse major from CUDA_PATH like ...\v13.0 or ...\CUDA\13 + $pathMatch = [regex]::Match($env:CUDA_PATH, 'v?(\d+)(?:\.\d+)?') + if ($pathMatch.Success) { return $pathMatch.Groups[1].Value } + } + return '13' +} + +function Get-CudaVersion { + <# + .SYNOPSIS + Gets the CUDA major.minor version for this container instance (e.g. + '12.9' or '13.0'). Defaults to '13.0' if no match can be found. + #> + if ($env:CUDA_PATH) { + $nvcc = Join-Path $env:CUDA_PATH "bin/nvcc.exe" + if (Test-Path $nvcc) { + $out = & $nvcc --version 2>&1 + $text = ($out -join "`n") + if ($text -match 'release\s+(\d+\.\d+)') { return $Matches[1] } + } + # Fallback: parse major.minor from CUDA_PATH like ...\v13.0 + $pathMatch = [regex]::Match($env:CUDA_PATH, 'v?(\d+\.\d+)') + if ($pathMatch.Success) { return $pathMatch.Groups[1].Value } + } + return '13.0' +} + +function Get-CtkTestMode { + <# + .SYNOPSIS + Validates and normalizes a CTK test mode passed as -Mode (forwarded from + the -ctk-mode arg): 'pinned' (default; empty means pinned), 'latest', or + 'sysctk'. Throws on any other value (fail loud on a typo'd mode). Returns + the lowercased mode. + #> + param([string]$Mode = "") + if ([string]::IsNullOrEmpty($Mode)) { return "pinned" } + $Mode = $Mode.ToLowerInvariant() + if (-not ($Mode -in @("pinned", "latest", "sysctk"))) { + throw "Invalid ctk mode '$Mode' (expected pinned|latest|sysctk)" + } + return $Mode +} + +function Set-CtkPin { + <# + .SYNOPSIS + Configures cuda-toolkit pinning for this lane per the -Mode arg (see + Get-CtkTestMode): 'pinned' (default) pins cuda-toolkit to the container's + CTK major.minor via PIP_CONSTRAINT; 'latest' and 'sysctk' leave it + unpinned ('sysctk' installs no cuda-toolkit wheel at all -- the + system-provided toolkit is used). + #> + param([string]$Mode = "") + if ((Get-CtkTestMode $Mode) -eq "pinned") { + $cudaVersion = Get-CudaVersion + $env:PIP_CONSTRAINT = Join-Path ([System.IO.Path]::GetTempPath()) "ctk-constraint.txt" + "cuda-toolkit==$cudaVersion.*" | Out-File -FilePath $env:PIP_CONSTRAINT -Encoding ascii + } else { + # latest / sysctk: no pin. Clear any inherited constraint so it cannot + # affect the resolve. + Remove-Item Env:\PIP_CONSTRAINT -ErrorAction SilentlyContinue + } +} + +function Get-CtkExtraFlavor { + <# + .SYNOPSIS + Returns the pip-extra toolkit "flavor" for the given -Mode: 'sysctk' when + the mode is sysctk (rely on the system-provided CUDA toolkit) or 'cu' + otherwise (pip-installed toolkit). Combine with the CUDA major, e.g. + "minimal-$(Get-CtkExtraFlavor $CtkMode)$cudaMajor". + #> + param([string]$Mode = "") + if ((Get-CtkTestMode $Mode) -eq "sysctk") { return "sysctk" } + return "cu" +} + +function Convert-ToUnixPath { + Param([Parameter(Mandatory = $true)][string]$p) + return ($p -replace "\\", "/") +} + +function Get-CudaCcclWheel { + <# + .SYNOPSIS + Returns the path of the cuda-cccl wheel artifact to use in the context + of a GitHub Actions CI test script. + #> + Param() + + $repoRoot = Get-RepoRoot + if ($env:GITHUB_ACTIONS) { + Push-Location $repoRoot + try { + $wheelArtifactName = (& bash -lc "ci/util/workflow/get_wheel_artifact_name.sh").Trim() + if (-not $wheelArtifactName) { throw 'Failed to resolve wheel artifact name' } + $repoRootPosix = Convert-ToUnixPath $repoRoot + # Ensure output from downloader goes to console, not function return pipeline + $null = (& bash -lc "ci/util/artifacts/download.sh $wheelArtifactName $repoRootPosix" 2>&1 | Out-Host) + if ($LASTEXITCODE -ne 0) { throw "Failed to download wheel artifact '$wheelArtifactName'" } + } + finally { Pop-Location } + } + + $wheelhouse = Join-Path $repoRoot 'wheelhouse' + $wheelPath = Get-OnePathMatch -Path $wheelhouse -Pattern '^cuda_cccl-.*\.whl' -File + return $wheelPath +} + +function Get-OnePathMatch { + <# + .SYNOPSIS + Returns a single path (file or directory) match for a given pattern, + throwing an error if there were no matches or more than one match. + #> + [CmdletBinding(DefaultParameterSetName = 'FileSet')] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $Pattern, + + [Parameter(Mandatory, ParameterSetName = 'FileSet')] + [switch] $File, + + [Parameter(Mandatory, ParameterSetName = 'DirSet')] + [switch] $Directory, + + [switch] $Recurse + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + throw "Path not found or not a directory: $Path" + } + + $gciArgs = @{ + LiteralPath = $Path + ErrorAction = 'SilentlyContinue' + } + + if ($Recurse) { $gciArgs['Recurse'] = $true } + if ($PSCmdlet.ParameterSetName -eq 'FileSet') { + $gciArgs['File'] = $true + } + else { + $gciArgs['Directory'] = $true + } + + $pathMatches = @( + Get-ChildItem @gciArgs | + Where-Object { $_.Name -match $Pattern } | + Select-Object -ExpandProperty FullName + ) + + if ($pathMatches.Count -ne 1) { + $kind = if ($PSCmdlet.ParameterSetName -eq 'FileSet') { 'file' } + else { 'directory' } + $indented = ($pathMatches | ForEach-Object { " $_" }) -join "`n" + + $msg = @" +Expected exactly one $kind name matching regex: + $Pattern +under: + $Path +Found: + $($pathMatches.Count) + +$indented +"@ + throw $msg + } + + return $pathMatches[0] +} + +Export-ModuleMember -Function Get-Python, Get-CudaMajor, Set-CtkPin, Get-CtkExtraFlavor, Convert-ToUnixPath, Get-RepoRoot, Get-CudaCcclWheel, Get-OnePathMatch diff --git a/cccl_upstream/ci/windows/build_cub.ps1 b/cccl_upstream/ci/windows/build_cub.ps1 new file mode 100644 index 00000000..37db55dc --- /dev/null +++ b/cccl_upstream/ci/windows/build_cub.ps1 @@ -0,0 +1,72 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "", + [Parameter(Mandatory = $false)] + [Alias("no-lid")] + [switch]$NO_LID_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid0")] + [switch]$LID0_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid1")] + [switch]$LID1_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid2")] + [switch]$LID2_SWITCH = $false +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cub" +$artifactTags = @() + +if ($NO_LID_SWITCH) { + $artifactTags += "no_lid" + $PRESET = "cub-nolid" +} elseif ($LID0_SWITCH) { + $artifactTags += "lid_0" + $PRESET = "cub-lid0" +} elseif ($LID1_SWITCH) { + $artifactTags += "lid_1" + $PRESET = "cub-lid1" +} elseif ($LID2_SWITCH) { + $artifactTags += "lid_2" + $PRESET = "cub-lid2" +} +$LOCAL_CMAKE_OPTIONS = "-DCMAKE_CXX_STANDARD=$CXX_STANDARD -DCMAKE_CUDA_STANDARD=$CXX_STANDARD" + +if ($CL_VERSION -lt [version]"19.20") { + $LOCAL_CMAKE_OPTIONS = "$LOCAL_CMAKE_OPTIONS -DCCCL_IGNORE_DEPRECATED_COMPILER=ON" +} + +configure_and_build_preset "CUB" $PRESET $LOCAL_CMAKE_OPTIONS + +if ($env:GITHUB_ACTIONS) { + Write-Host "Packaging test artifacts..." + if ($artifactTags.Count -gt 0) { + & bash "./upload_cub_test_artifacts.sh" @artifactTags + } else { + & bash "./upload_cub_test_artifacts.sh" + } +} + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/build_cuda_cccl_python.ps1 b/cccl_upstream/ci/windows/build_cuda_cccl_python.ps1 new file mode 100644 index 00000000..e151c673 --- /dev/null +++ b/cccl_upstream/ci/windows/build_cuda_cccl_python.ps1 @@ -0,0 +1,422 @@ +<# +.SYNOPSIS + Build Python cuda-cccl wheels on Windows. + +.DESCRIPTION + This script is the Windows analog to the Linux ../build_cuda_cccl_python.sh + script. It is responsible for building CUDA 12.x and CUDA 13.x wheels that + are then merged together into a singular cuda-cccl wheel. + + A single CUDA 12.9 builder image (i.e. Docker devcontainer) is used to + build each distinct Python/MSVC combo. Much like the Linux approach, this + script detects when launched via the outer 12.9 instance, builds a `cu12` + wheel, then dispatches a inner Docker instance (Docker-out-of-Docker) to + execute this script with `-OnlyCudaMajor 13 -SkipUpload` parameters, which + yields a `cu13` build. + + Upon completion of the `cu13` build, the outer 12.9 container merges both + `cu12` and `cu13` wheels into a single cuda-cccl wheel, and uploads that + via the standard CCCL CI artifact upload mechanisms. + +.PARAMETER PyVersion + **Required.** The Python version to use for building the wheel, expressed + as `.` (e.g. `3.11`) or a free-threaded version such as + `3.14t`. + +.PARAMETER OnlyCudaMajor + Optional. Restricts the build to a single CUDA major version (`12` or `13`). + When set, only that version is built and the *merge* step is skipped. + +.PARAMETER Cuda13Image + Optional. The Docker image name used for a nested build of the CUDA 13 + wheel when the outer container defaults to CUDA 12.9. The default value + matches the RAPIDS dev-container image that contains the required + toolchain: `rapidsai/devcontainers:26.06-cuda13.0-cl14.44-windows2022`. + +.PARAMETER SkipUpload + When set, prevents the final wheel(s) from being uploaded as a GitHub + Actions artifact even when the script detects it is running inside an + Action. + +.EXAMPLE + # Build a single cuda-cccl wheel for Python 3.13 (consisting of both CUDA + # 12 and 13 versions), and, if in CI, upload the resulting wheel as an + # artifact. + .\build_cuda_cccl_python.ps1 -PyVersion 3.11 +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory = $true)] + [Alias("py-version")] + [ValidatePattern("^\d+\.\d+t?$")] + [string]$PyVersion, + + [Parameter(Mandatory = $false)] + [ValidateSet('12', '13')] + [string]$OnlyCudaMajor, + + [Parameter(Mandatory = $false)] + [string]$Cuda13Image = "rapidsai/devcontainers:26.06-cuda13.0-cl14.44-windows2022", + + [Parameter(Mandatory = $false)] + [switch]$SkipUpload +) + +$ErrorActionPreference = "Stop" + +# Import shared helpers. +Import-Module "$PSScriptRoot/build_common.psm1" +Import-Module "$PSScriptRoot/build_common_python.psm1" -Force + +# Resolve repo root from this script's location. +$RepoRoot = Resolve-Path "$PSScriptRoot/../.." +Write-Host "Repo root: $RepoRoot" + +# Get the full path to the python.exe for the version we need. +Write-Host "Looking for Python version $PyVersion..." +$PythonExe = Get-Python -Version $PyVersion +Write-Host "Using Python: $PythonExe" +& $PythonExe -m pip --version + +# Ensure MSVC is available. +$clPath = (Get-Command cl).Source +if (-not $clPath) { + throw "cl.exe not found in PATH. Run from a Developer PowerShell prompt." +} +Write-Host "Found cl.exe at: $clPath" + +function Resolve-CudaPathForMajor { + Param( + [Parameter(Mandatory = $true)] + [ValidateSet('12', '13')] + [string]$Major + ) + $candidates = @() + Get-ChildItem Env: | + Where-Object { $_.Name -match "^CUDA_PATH_V${Major}_(\d+)$" } | + ForEach-Object { + $minor = [int]([regex]::Match( + $_.Name, + "^CUDA_PATH_V${Major}_(\d+)$" + ).Groups[1].Value) + $candidates += [PSCustomObject]@{ + Minor = $minor; + Path = $_.Value + } + } + + if ($candidates.Count -gt 0) { + return ($candidates | Sort-Object -Property Minor -Descending | + Select-Object -First 1).Path + } + + if ($env:CUDA_PATH) { + $maybe = $env:CUDA_PATH + $nvcc = Join-Path $maybe 'bin/nvcc.exe' + if (Test-Path $nvcc) { + $out = & $nvcc --version 2>&1 + $text = ($out -join "`n") + if ($text -match 'release\s+(\d+)\.') { + if ($Matches[1] -eq $Major) { + return $maybe + } + } + } + } + + return $null +} + +# If $OnlyCudaMajor is present, it means we're being launched from a +# nested Docker container build (12.x launched a 13.x build via DooD). +if ($OnlyCudaMajor) { + $CudaMajorsToBuild = @($OnlyCudaMajor) +} +else { + $CudaMajorsToBuild = @('12', '13') +} +$DoMerge = -not [bool]$OnlyCudaMajor + +# Base pip/CMake options +$pipBaseConfigArgs = @( + '-C', 'cmake.define.CMAKE_C_COMPILER=cl.exe', + '-C', 'cmake.define.CMAKE_CXX_COMPILER=cl.exe' +) + +$env:CMAKE_GENERATOR = "Ninja" + +# Ensure wheelhouse directories exist. +$Wheelhouse = Join-Path $RepoRoot "wheelhouse" +New-Item -ItemType Directory -Path $Wheelhouse -Force | Out-Null +${null} = New-Item -ItemType Directory -Path (Join-Path $RepoRoot 'wheelhouse_cu12') -Force +${null} = New-Item -ItemType Directory -Path (Join-Path $RepoRoot 'wheelhouse_cu13') -Force + +function Invoke-Cuda13NestedBuild { + <# + .SYNOPSIS + Run the nested Docker build for CUDA 13 when we are already inside a + CUDA 12 builder image. + + .DESCRIPTION + This routine launches a Docker devcontainer CUDA 13 build for the given + Python version by way of Docker-out-of-Docker (DooD) facilities. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] [string] $Cuda13Image, + [Parameter(Mandatory)] [string] $PyVersion, + [ValidateNotNullOrEmpty()] [string] $HostWorkspace = $env:HOST_WORKSPACE, + [ValidateNotNullOrEmpty()] [string] $ContainerWorkspace = $env:CONTAINER_WORKSPACE + ) + + # Validate required environment variables. + if (-not $HostWorkspace) { + throw "HOST_WORKSPACE env var is not set; required for DooD " + + "nested docker mounts on Windows." + } + if (-not $ContainerWorkspace) { + throw "CONTAINER_WORKSPACE env var is not set; required for " + + "DooD nested docker mounts on Windows." + } + + # Validate Docker CLI availability. + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + throw "docker CLI not found in the devcontainer image (required for DooD)." + } + + Write-Host "Checking DooD connectivity..." + $dockerVersionOutput = & docker version 2>&1 + $dockerExitCode = $LASTEXITCODE + $dockerVersionOutput | Out-Host + if ($dockerExitCode -ne 0) { + throw "DooD connectivity check failed (exit code $dockerExitCode). See Docker output above." + } + Write-Host "DooD appears to be working, continuing..." + + # Detect outer-container resources so we can set sensible limits. + $os = Get-WmiObject -Class Win32_OperatingSystem + $totalGB = [math]::Floor($os.TotalVisibleMemorySize / 1MB) # KB -> GB + $procCount = [Environment]::ProcessorCount + + # Leave a little head-room so the outer container doesn't starve + $memLimitGB = [math]::Max(2, [int]([math]::Floor($totalGB * 0.9))) + $cpuCount = [math]::Max(2, $procCount) + + Write-Host "Launching nested Docker for CUDA 13 build using image: $Cuda13Image" + $targetFile = Join-Path $ContainerWorkspace 'ci\windows\build_cuda_cccl_python.ps1' + $dockerArgs = @( + 'run', '--rm', '-i', + '--cpu-count', "$cpuCount", + '--memory', "${memLimitGB}g", + '--workdir', $ContainerWorkspace, + '--mount', "type=bind,source=$HostWorkspace,target=$ContainerWorkspace", + '--env', "py_version=$PyVersion", + '--env', "GITHUB_ACTIONS=$($env:GITHUB_ACTIONS)", + '--env', "GITHUB_RUN_ID=$($env:GITHUB_RUN_ID)", + '--env', "JOB_ID=$($env:JOB_ID)", + $Cuda13Image, + 'PowerShell.exe', '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', $targetFile, + '-py-version', $PyVersion, + '-OnlyCudaMajor', '13', + '-SkipUpload' + ) + + Write-Host ("About to invoke: docker " + ($dockerArgs -join ' ')) + Invoke-Checked { & docker @dockerArgs } 'Nested CUDA 13 wheel build failed' +} + +function Build-CudaCcclWheel { + <# + .SYNOPSIS + Perform the regular wheel build for a given CUDA major version. + + .DESCRIPTION + This routine is used to build both CUDA 12 and CUDA 13 based wheels, + and is called from normal "outer" Docker containers, as well as the + "inner" nested ones. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] [ValidateSet('12', '13')] [string] $Major, + [Parameter(Mandatory)] [string] $RepoRoot, + [Parameter(Mandatory)] [string] $PythonExe, + [Parameter(Mandatory)] [string[]] $PipBaseConfigArgs + ) + + # Resolve CUDA toolkit location for the requested major version. + $CudaPathForMajor = Resolve-CudaPathForMajor -Major $Major + if (-not $CudaPathForMajor) { + throw "CUDA Toolkit $Major not found. Ensure CUDA_PATH_V${Major}_* " + + "is set or matching toolkit is installed." + } + + $NvccForMajor = Join-Path $CudaPathForMajor 'bin/nvcc.exe' + if (-not (Test-Path $NvccForMajor)) { + throw "nvcc not found at $NvccForMajor" + } + + # Convert Windows paths to Unix-style for CMake + $NvccUnix = Convert-ToUnixPath $NvccForMajor + $CudaUnix = Convert-ToUnixPath $CudaPathForMajor + + # Build the pip configuration arguments that inject the CUDA toolchain. + $pipConfigArgs = $PipBaseConfigArgs + @( + '-C', "cmake.define.CMAKE_CUDA_COMPILER=$NvccUnix", + '-C', "cmake.define.CUDAToolkit_ROOT=$CudaUnix" + ) + + $extra = "cu$Major" + # Use separate output directories for 12 vs 13. + $outDir = Join-Path $RepoRoot "wheelhouse_$extra" + + Write-Host "Building cuda-cccl wheel for CUDA $Major at $CudaPathForMajor..." + + # Run pip wheel to build the wheel. + $pythonArgs = @( + '-m', 'pip', 'wheel', + '-w', $outDir, + ".[${extra}]", + '-v' + ) + $pipConfigArgs + + Write-Host ("python " + ($pythonArgs -join ' ')) + Invoke-Checked { & $PythonExe @pythonArgs } "Wheel build failed for CUDA $Major" + + # Normalise the wheel filename (append .cu12/.cu13) and prune duplicates. + $builtWheel = Get-OnePathMatch -Path $outDir ` + -Pattern '^cuda_cccl-.*\.whl' ` + -File + if (-not $builtWheel) { + throw "Failed to locate built wheel in $outDir for CUDA $Major" + } + + $builtName = [System.IO.Path]::GetFileName($builtWheel) + if ($builtName -notmatch ".cu$Major\.whl$") { + $newName = ([System.IO.Path]::GetFileNameWithoutExtension($builtName)) ` + + ".cu$Major.whl" + Write-Host "Renaming wheel to: $newName" + Rename-Item -Path $builtWheel -NewName $newName -Force + } + + # Remove any stray wheels that lack the .cuXX suffix. + Get-ChildItem -Path $outDir -Filter 'cuda_cccl-*.whl' | + Where-Object { $_.Name -notmatch "\.cu$Major\.whl$" } | + ForEach-Object { + Write-Host "Removing duplicate wheel: $($_.FullName)" + Remove-Item -Force $_.FullName + } +} + +# Main build entry code. +Push-Location (Join-Path $RepoRoot 'python/cuda_cccl') +try { + foreach ($major in $CudaMajorsToBuild) { + + # Nested Docker build for CUDA 13 for when we are currently inside a + # CUDA 12 image. + if (-not $OnlyCudaMajor -and $major -eq '13' -and $Cuda13Image) { + Invoke-Cuda13NestedBuild ` + -Cuda13Image $Cuda13Image ` + -PyVersion $PyVersion + + continue + } + + # Perform a normal build for the current major version. This may + # be invoked from either an "outer" or inner "nested" image. + Build-CudaCcclWheel ` + -Major $major ` + -RepoRoot $RepoRoot ` + -PythonExe $PythonExe ` + -PipBaseConfigArgs $pipBaseConfigArgs + } +} +finally { + Pop-Location +} + + +# Merge the two major-version wheels (if both were built). This will fail if +# either wheel can't be found. This only runs on the outer (non-nested) +# container image. +if ($DoMerge) { + + $Cu12Wheel = Get-OnePathMatch ` + -Path (Join-Path $RepoRoot 'wheelhouse_cu12') ` + -Pattern '^cuda_cccl-.*\.cu12\.whl' ` + -File + + $Cu13Wheel = Get-OnePathMatch ` + -Path (Join-Path $RepoRoot 'wheelhouse_cu13') ` + -Pattern '^cuda_cccl-.*\.cu13\.whl' ` + -File + + Write-Host "Found CUDA 12 wheel: $Cu12Wheel" + Write-Host "Found CUDA 13 wheel: $Cu13Wheel" + + Write-Host 'Merging CUDA wheels...' + Invoke-Checked { & $PythonExe -m pip install wheel | Write-Host } 'Failed to install wheel for merging' + + $WheelhouseMerged = Join-Path $RepoRoot 'wheelhouse_merged' + ${null} = New-Item -ItemType Directory -Path $WheelhouseMerged -Force + + $mergePy = Join-Path $RepoRoot 'python/cuda_cccl/merge_cuda_wheels.py' + Invoke-Checked { & $PythonExe $mergePy $Cu12Wheel $Cu13Wheel --output-dir $WheelhouseMerged } 'Merging wheels failed' + + # Clean up the per-major directories and move the merged wheel into the + # final location. + Get-ChildItem $Wheelhouse -Filter '*.whl' | + ForEach-Object { + Remove-Item -Force $_.FullName + } + $MergedWheel = Get-OnePathMatch ` + -Path $WheelhouseMerged ` + -Pattern '^cuda_cccl-.*\.whl' ` + -File + Move-Item -Force $MergedWheel $Wheelhouse + + Remove-Item $WheelhouseMerged -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item (Join-Path $RepoRoot 'wheelhouse_cu12') ` + -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item (Join-Path $RepoRoot 'wheelhouse_cu13') ` + -Recurse -Force -ErrorAction SilentlyContinue + + Write-Host 'Final wheels in wheelhouse:' + Get-ChildItem $Wheelhouse -Filter '*.whl' | + ForEach-Object { + Write-Host " - $($_.Name)" + } +} + +# If it turns out we need delvewheel, we'd handle it here, after the merging +# of wheels. The two DLLs that seem like they might be problematic are +# msvc140p.dll, and dbghelp.dll. The former comes from llvmlite, upon which +# we depend. Dbghelp.dll ships in C:\Windows\System32, but that will often +# be a much older version compared to the one used by Visual Studio. We only +# use one symbol from Dbghelp.dll: UnDecorateSymbolName, which is used by +# nvrtc. If we encounter weird issues with c.parallel jit compilation and +# nvrtc in the wild on Windows, an out-of-date Dbghelp.dll could possibly be +# the culprit. +# +# For now, though, it doesn't appear to be necessary. + +# Optionally upload the wheel artifact. +if ($env:GITHUB_ACTIONS -and -not $SkipUpload) { + Push-Location $RepoRoot + try { + Write-Host 'GITHUB_ACTIONS detected; uploading wheel artifact' + $wheelArtifactName = (& bash -lc "ci/util/workflow/get_wheel_artifact_name.sh").Trim() + if (-not $wheelArtifactName) { + throw 'Failed to resolve wheel artifact name' + } + Write-Host "Wheel artifact name: $wheelArtifactName" + + $uploadCmd = "ci/util/artifacts/upload.sh $wheelArtifactName 'wheelhouse/.*'" + Invoke-Checked { & bash -lc $uploadCmd } 'Wheel artifact upload failed' + } + finally { + Pop-Location + } +} diff --git a/cccl_upstream/ci/windows/build_cudax.ps1 b/cccl_upstream/ci/windows/build_cudax.ps1 new file mode 100644 index 00000000..e7c44084 --- /dev/null +++ b/cccl_upstream/ci/windows/build_cudax.ps1 @@ -0,0 +1,31 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(20)] + [int]$CXX_STANDARD = 20, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Remove-Module -Name build_common -ErrorAction SilentlyContinue +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cudax" +$LOCAL_CMAKE_OPTIONS = "-DCMAKE_CXX_STANDARD=$CXX_STANDARD -DCMAKE_CUDA_STANDARD=$CXX_STANDARD" + +configure_and_build_preset "CUDA Experimental" $PRESET $LOCAL_CMAKE_OPTIONS + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/build_libcudacxx.ps1 b/cccl_upstream/ci/windows/build_libcudacxx.ps1 new file mode 100644 index 00000000..6b0aff2e --- /dev/null +++ b/cccl_upstream/ci/windows/build_libcudacxx.ps1 @@ -0,0 +1,48 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "libcudacxx" +$LOCAL_CMAKE_OPTIONS = "-DCMAKE_CXX_STANDARD=$CXX_STANDARD -DCMAKE_CUDA_STANDARD=$CXX_STANDARD" + +$uploadTestArtifacts = $false +if ($env:GITHUB_ACTIONS) { + & bash "./util/workflow/has_consumers.sh" + $uploadTestArtifacts = $LASTEXITCODE -eq 0 + if ($uploadTestArtifacts) { + $env:LIT_OPTS = "$env:LIT_OPTS -Dtest_executable_mode=build".Trim() + } +} + +configure_and_build_preset "libcudacxx" $PRESET $LOCAL_CMAKE_OPTIONS + +if ($uploadTestArtifacts) { + Write-Host "Packaging test artifacts..." + Invoke-Checked { + & bash "./upload_libcudacxx_test_artifacts.sh" + } "Packaging test artifacts failed" +} + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/build_thrust.ps1 b/cccl_upstream/ci/windows/build_thrust.ps1 new file mode 100644 index 00000000..840f1177 --- /dev/null +++ b/cccl_upstream/ci/windows/build_thrust.ps1 @@ -0,0 +1,37 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "thrust" +$LOCAL_CMAKE_OPTIONS = "-DCMAKE_CXX_STANDARD=$CXX_STANDARD -DCMAKE_CUDA_STANDARD=$CXX_STANDARD" + +configure_and_build_preset "Thrust" $PRESET $LOCAL_CMAKE_OPTIONS + +if ($env:GITHUB_ACTIONS) { + Write-Host "Packaging test artifacts..." + & bash "./upload_thrust_test_artifacts.sh" +} + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/run_cpu_bisect.ps1 b/cccl_upstream/ci/windows/run_cpu_bisect.ps1 new file mode 100644 index 00000000..911d38d9 --- /dev/null +++ b/cccl_upstream/ci/windows/run_cpu_bisect.ps1 @@ -0,0 +1,41 @@ +Param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PassthroughArgs +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +if ($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +if ($null -eq $PassthroughArgs) { + $PassthroughArgs = @() +} + +Import-Module "$PSScriptRoot/build_common.psm1" + +$ciDirWindows = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$repoDirWindows = (Resolve-Path (Join-Path $ciDirWindows "..")).Path + +$ciDir = $ciDirWindows -replace "\\", "/" +$repoDir = $repoDirWindows -replace "\\", "/" +$utilScript = "$ciDir/util/git_bisect.sh" + +$argString = if ($PassthroughArgs.Count -gt 0) { " " + ($PassthroughArgs -join " ") } else { "" } + +$bashCommand = "cd $repoDir; $utilScript$argString" +Write-Host $bashCommand -ForegroundColor Blue + +& bash -lc $bashCommand +$exitCode = $LASTEXITCODE + +if ($CURRENT_PATH -ne "ci") { + popd +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/cccl_upstream/ci/windows/run_cpu_target.ps1 b/cccl_upstream/ci/windows/run_cpu_target.ps1 new file mode 100644 index 00000000..db928595 --- /dev/null +++ b/cccl_upstream/ci/windows/run_cpu_target.ps1 @@ -0,0 +1,41 @@ +Param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PassthroughArgs +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +if ($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +if ($null -eq $PassthroughArgs) { + $PassthroughArgs = @() +} + +Import-Module "$PSScriptRoot/build_common.psm1" + +$ciDirWindows = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$repoDirWindows = (Resolve-Path (Join-Path $ciDirWindows "..")).Path + +$ciDir = $ciDirWindows -replace "\\", "/" +$repoDir = $repoDirWindows -replace "\\", "/" +$utilScript = "$ciDir/util/build_and_test_targets.sh" + +$argString = if ($PassthroughArgs.Count -gt 0) { " " + ($PassthroughArgs -join " ") } else { "" } + +$bashCommand = "cd $repoDir; $utilScript$argString" +Write-Host $bashCommand -ForegroundColor Blue + +& bash -lc $bashCommand +$exitCode = $LASTEXITCODE + +if ($CURRENT_PATH -ne "ci") { + popd +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/cccl_upstream/ci/windows/run_gpu_bisect.ps1 b/cccl_upstream/ci/windows/run_gpu_bisect.ps1 new file mode 100644 index 00000000..911d38d9 --- /dev/null +++ b/cccl_upstream/ci/windows/run_gpu_bisect.ps1 @@ -0,0 +1,41 @@ +Param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PassthroughArgs +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +if ($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +if ($null -eq $PassthroughArgs) { + $PassthroughArgs = @() +} + +Import-Module "$PSScriptRoot/build_common.psm1" + +$ciDirWindows = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$repoDirWindows = (Resolve-Path (Join-Path $ciDirWindows "..")).Path + +$ciDir = $ciDirWindows -replace "\\", "/" +$repoDir = $repoDirWindows -replace "\\", "/" +$utilScript = "$ciDir/util/git_bisect.sh" + +$argString = if ($PassthroughArgs.Count -gt 0) { " " + ($PassthroughArgs -join " ") } else { "" } + +$bashCommand = "cd $repoDir; $utilScript$argString" +Write-Host $bashCommand -ForegroundColor Blue + +& bash -lc $bashCommand +$exitCode = $LASTEXITCODE + +if ($CURRENT_PATH -ne "ci") { + popd +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/cccl_upstream/ci/windows/run_gpu_target.ps1 b/cccl_upstream/ci/windows/run_gpu_target.ps1 new file mode 100644 index 00000000..db928595 --- /dev/null +++ b/cccl_upstream/ci/windows/run_gpu_target.ps1 @@ -0,0 +1,41 @@ +Param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PassthroughArgs +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +if ($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +if ($null -eq $PassthroughArgs) { + $PassthroughArgs = @() +} + +Import-Module "$PSScriptRoot/build_common.psm1" + +$ciDirWindows = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$repoDirWindows = (Resolve-Path (Join-Path $ciDirWindows "..")).Path + +$ciDir = $ciDirWindows -replace "\\", "/" +$repoDir = $repoDirWindows -replace "\\", "/" +$utilScript = "$ciDir/util/build_and_test_targets.sh" + +$argString = if ($PassthroughArgs.Count -gt 0) { " " + ($PassthroughArgs -join " ") } else { "" } + +$bashCommand = "cd $repoDir; $utilScript$argString" +Write-Host $bashCommand -ForegroundColor Blue + +& bash -lc $bashCommand +$exitCode = $LASTEXITCODE + +if ($CURRENT_PATH -ne "ci") { + popd +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/cccl_upstream/ci/windows/test_cccl_c_parallel.ps1 b/cccl_upstream/ci/windows/test_cccl_c_parallel.ps1 new file mode 100644 index 00000000..593a3bec --- /dev/null +++ b/cccl_upstream/ci/windows/test_cccl_c_parallel.ps1 @@ -0,0 +1,31 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +# Build first +$buildCmd = "$PSScriptRoot/build_cccl_c_parallel.ps1 -arch '$CUDA_ARCH' -cmake-options '$CMAKE_OPTIONS'" +Write-Host "Running: $buildCmd" +Invoke-Expression $buildCmd + +Remove-Module -Name build_common -ErrorAction SilentlyContinue +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @(20, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cccl-c-parallel" +test_preset "CCCL C Parallel" "$PRESET" + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_cccl_c_parallel_v2.ps1 b/cccl_upstream/ci/windows/test_cccl_c_parallel_v2.ps1 new file mode 100644 index 00000000..73d3a4d5 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cccl_c_parallel_v2.ps1 @@ -0,0 +1,30 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Remove-Module -Name build_common -ErrorAction SilentlyContinue +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @(20, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cccl-c-parallel-v2" +$LOCAL_CMAKE_OPTIONS = "" + +configure_and_build_preset "CCCL C Parallel v2 (HostJIT)" $PRESET $LOCAL_CMAKE_OPTIONS + +test_preset "CCCL C Parallel v2 (HostJIT)" "$PRESET" + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_cub.ps1 b/cccl_upstream/ci/windows/test_cub.ps1 new file mode 100644 index 00000000..2685e145 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cub.ps1 @@ -0,0 +1,73 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("no-lid")] + [switch]$NO_LID_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid0")] + [switch]$LID0_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid1")] + [switch]$LID1_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("lid2")] + [switch]$LID2_SWITCH = $false, + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cub" +$artifactTag = "" +$variantArg = "" +if ($NO_LID_SWITCH) { + $artifactTag = "no_lid" + $PRESET = "cub-nolid" + $variantArg = "-no-lid" +} elseif ($LID0_SWITCH) { + $artifactTag = "lid_0" + $PRESET = "cub-lid0" + $variantArg = "-lid0" +} elseif ($LID1_SWITCH) { + $artifactTag = "lid_1" + $PRESET = "cub-lid1" + $variantArg = "-lid1" +} elseif ($LID2_SWITCH) { + $artifactTag = "lid_2" + $PRESET = "cub-lid2" + $variantArg = "-lid2" +} + +if ($env:GITHUB_ACTIONS -and $artifactTag) { + $producerId = (& bash "./util/workflow/get_producer_id.sh").Trim() + $artifactName = "z_cub-test-artifacts-$env:DEVCONTAINER_NAME-$producerId-$artifactTag" + Write-Host "Unpacking artifact '$artifactName'" + & bash "./util/artifacts/download_packed.sh" "$artifactName" "../" +} else { + $buildCmd = "$PSScriptRoot/build_cub.ps1 -std $CXX_STANDARD -arch '$CUDA_ARCH' -cmake-options '$CMAKE_OPTIONS' $variantArg" + Write-Host "Running: $buildCmd" + Invoke-Expression $buildCmd +} + +test_preset "CUB ($PRESET)" "$PRESET" + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_cuda_cccl_examples_python.ps1 b/cccl_upstream/ci/windows/test_cuda_cccl_examples_python.ps1 new file mode 100644 index 00000000..bbc9d1bc --- /dev/null +++ b/cccl_upstream/ci/windows/test_cuda_cccl_examples_python.ps1 @@ -0,0 +1,50 @@ +Param( + [Parameter(Mandatory = $true)] + [Alias("py-version")] + [ValidatePattern("^\d+\.\d+t?$")] + [string]$PyVersion, + + [Alias("ctk-mode")] + [string]$CtkMode = "" +) + +$ErrorActionPreference = "Stop" + +# Import shared helpers +Import-Module "$PSScriptRoot/build_common.psm1" +Import-Module "$PSScriptRoot/build_common_python.psm1" + +$python = Get-Python -Version $PyVersion +$cudaMajor = Get-CudaMajor +$ctkFlavor = Get-CtkExtraFlavor $CtkMode + +# Pin cuda-toolkit to the container's CTK minor (-ctk-mode latest +# opts out). See build_common_python.psm1. +Set-CtkPin $CtkMode + +$repoRoot = Get-RepoRoot + +${wheelPath} = Get-CudaCcclWheel + +# pytest-benchmark is for the host-benchmark smoke test below. +Invoke-Checked { & $python -m pip install -U pip pytest pytest-xdist pytest-benchmark } "Failed to install pytest / pytest-xdist / pytest-benchmark" +# CuPy is required by the cuda.compute examples and is not part of the test extras +Invoke-Checked { & $python -m pip install "${wheelPath}[test-$ctkFlavor$cudaMajor]" "cupy-cuda${cudaMajor}x" } "Failed to install cuda_cccl test extra / cupy" + +Push-Location (Join-Path $repoRoot "python/cuda_cccl/tests") +try { + Invoke-Checked { & $python -m pytest -n 6 test_examples.py } "examples tests failed" +} +finally { Pop-Location } + +# Smoke-test the host-overhead benchmark harness: run every benchmark case +# exactly once (pass/fail only, no timing) so harness rot fails CI here instead +# of silently surviving until someone runs the perf suite. --benchmark-disable +# makes pytest-benchmark invoke each benchmarked callable a single time. This +# lane already installs cupy + numba (for the examples), which the benchmark +# suite also needs, so only pytest-benchmark is added above. +Push-Location (Join-Path $repoRoot "python/cuda_cccl/benchmarks/compute/host") +try { + Invoke-Checked { & $python -m pytest -v --benchmark-disable . } "host benchmark smoke test failed" +} +finally { Pop-Location } diff --git a/cccl_upstream/ci/windows/test_cuda_cccl_headers_python.ps1 b/cccl_upstream/ci/windows/test_cuda_cccl_headers_python.ps1 new file mode 100644 index 00000000..4176bc04 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cuda_cccl_headers_python.ps1 @@ -0,0 +1,36 @@ +Param( + [Parameter(Mandatory = $true)] + [Alias("py-version")] + [ValidatePattern("^\d+\.\d+t?$")] + [string]$PyVersion, + + [Alias("ctk-mode")] + [string]$CtkMode = "" +) + +$ErrorActionPreference = "Stop" + +# Import shared helpers +Import-Module "$PSScriptRoot/build_common.psm1" +Import-Module "$PSScriptRoot/build_common_python.psm1" + +$python = Get-Python -Version $PyVersion +$cudaMajor = Get-CudaMajor +$ctkFlavor = Get-CtkExtraFlavor $CtkMode + +# Pin cuda-toolkit to the container's CTK minor (-ctk-mode latest +# opts out). See build_common_python.psm1. +Set-CtkPin $CtkMode + +$repoRoot = Get-RepoRoot + +${wheelPath} = Get-CudaCcclWheel + +Invoke-Checked { & $python -m pip install -U pip pytest pytest-xdist } "Failed to install pytest / pytest-xdist" +Invoke-Checked { & $python -m pip install "${wheelPath}[test-$ctkFlavor$cudaMajor]" } "Failed to install cuda_cccl test extra" + +Push-Location (Join-Path $repoRoot "python/cuda_cccl/tests") +try { + Invoke-Checked { & $python -m pytest -n auto -v headers/ } "headers tests failed" +} +finally { Pop-Location } diff --git a/cccl_upstream/ci/windows/test_cuda_compute_minimal_python.ps1 b/cccl_upstream/ci/windows/test_cuda_compute_minimal_python.ps1 new file mode 100644 index 00000000..1f64d670 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cuda_compute_minimal_python.ps1 @@ -0,0 +1,79 @@ +Param( + [Parameter(Mandatory = $true)] + [Alias("py-version")] + [ValidatePattern("^\d+\.\d+t?$")] + [string]$PyVersion, + + [Alias("ctk-mode")] + [string]$CtkMode = "" +) + +$ErrorActionPreference = "Stop" + +# Import shared helpers +Import-Module "$PSScriptRoot/build_common.psm1" +Import-Module "$PSScriptRoot/build_common_python.psm1" + +$python = Get-Python -Version $PyVersion +$cudaMajor = Get-CudaMajor +$ctkFlavor = Get-CtkExtraFlavor $CtkMode + +# Pin cuda-toolkit to the container's CTK minor (-ctk-mode latest +# opts out). See build_common_python.psm1. +Set-CtkPin $CtkMode + +$repoRoot = Get-RepoRoot + +$wheelPath = Get-CudaCcclWheel + +# Install cuda_cccl with the minimal CUDA extra. This intentionally avoids the +# full cu* extras because those pull in numba/numba-cuda. +Invoke-Checked { & $python -m pip install -U pip pytest pytest-xdist } "Failed to install pytest / pytest-xdist" +Invoke-Checked { & $python -m pip install "$wheelPath[minimal-$ctkFlavor$cudaMajor]" } "Failed to install cuda_cccl minimal extra" + +Push-Location (Join-Path $repoRoot "python/cuda_cccl/tests") +try { + Invoke-Checked { & $python -m pytest -n 6 -v compute/test_no_numba.py } "test_no_numba.py failed" + + if ($PyVersion -eq "3.14t") { + # Select only tests that support the minimal extra so pytest does not + # collect tests that import numba-cuda and re-enable the GIL. These tests + # provide their own worker threads, so keep pytest itself in a single + # process. The serialization node-ids are module-skipped on the v2 + # backend today and will start running there automatically once v2 gains + # serialization support. + Invoke-Checked { + & $python -m pytest -n 0 -v ` + compute/test_free_threading_stress.py ` + compute/test_multi_cc_serialization.py::test_aot_build_result_load_failure_is_shared_and_retryable ` + compute/test_multi_cc_serialization.py::test_aot_serialization_waits_for_canonical_first_load + } "free-threading stress / serialization tests failed" + + # Broad thread-safety sweep (pytest-run-parallel): re-run the numba-free + # functional suite with each test executed concurrently across threads + # (barrier-synchronized start), stressing the process-wide build cache, + # single-flight coordination, and the Cython bindings from many threads at + # once. Complements test_free_threading_stress.py above, which targets + # specific shared-object scenarios by hand. -n 0 so the threads share one + # interpreter. + # + # --parallel-threads=2 matches CuPy's free-threading CI (the closest GPU + # precedent); a small fixed count bounds GPU-memory pressure from + # concurrent kernels and stays reproducible across runners, unlike =auto + # (the runner's logical-core count). + # + # pytest-run-parallel is only used by this sweep, so install it on the + # 3.14t path rather than for every minimal (e.g. non-free-threaded 3.14) + # run. + Invoke-Checked { & $python -m pip install pytest-run-parallel } "Failed to install pytest-run-parallel" + + # Fail fast if the interpreter is not actually GIL-free (wrong build / + # PYTHON_GIL=1): pytest-run-parallel does NOT catch a GIL that is enabled + # from the start -- it would run threads GIL-serialized and pass + # vacuously. (A GIL *re-enabled mid-run* by a non-free-threaded import IS + # caught by the plugin, which is why we do not pass --ignore-gil-enabled.) + Invoke-Checked { & $python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is enabled; parallel sweep has no signal'" } "interpreter is not GIL-free; parallel sweep has no signal" + Invoke-Checked { & $python -m pytest -n 0 -v --parallel-threads=2 compute/test_no_numba.py } "parallel-threads sweep failed" + } +} +finally { Pop-Location } diff --git a/cccl_upstream/ci/windows/test_cuda_compute_python.ps1 b/cccl_upstream/ci/windows/test_cuda_compute_python.ps1 new file mode 100644 index 00000000..9283af57 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cuda_compute_python.ps1 @@ -0,0 +1,37 @@ +Param( + [Parameter(Mandatory = $true)] + [Alias("py-version")] + [ValidatePattern("^\d+\.\d+t?$")] + [string]$PyVersion, + + [Alias("ctk-mode")] + [string]$CtkMode = "" +) + +$ErrorActionPreference = "Stop" + +# Import shared helpers +Import-Module "$PSScriptRoot/build_common.psm1" +Import-Module "$PSScriptRoot/build_common_python.psm1" + +$python = Get-Python -Version $PyVersion +$cudaMajor = Get-CudaMajor +$ctkFlavor = Get-CtkExtraFlavor $CtkMode + +# Pin cuda-toolkit to the container's CTK minor (-ctk-mode latest +# opts out). See build_common_python.psm1. +Set-CtkPin $CtkMode + +$repoRoot = Get-RepoRoot + +$wheelPath = Get-CudaCcclWheel + +Invoke-Checked { & $python -m pip install -U pip pytest pytest-xdist } "Failed to install pytest / pytest-xdist" +Invoke-Checked { & $python -m pip install "$wheelPath[test-$ctkFlavor$cudaMajor]" } "Failed to install cuda_cccl test extra" + +Push-Location (Join-Path $repoRoot "python/cuda_cccl/tests") +try { + Invoke-Checked { & $python -m pytest -n 6 -v compute/ -m "not large and not free_threading" } "compute tests (not large) failed" + Invoke-Checked { & $python -m pytest -n 0 -v compute/ -m "large and not free_threading" } "compute tests (large) failed" +} +finally { Pop-Location } diff --git a/cccl_upstream/ci/windows/test_cudax.ps1 b/cccl_upstream/ci/windows/test_cudax.ps1 new file mode 100644 index 00000000..5c7e4873 --- /dev/null +++ b/cccl_upstream/ci/windows/test_cudax.ps1 @@ -0,0 +1,35 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(20)] + [int]$CXX_STANDARD = 20, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +# Build first +$buildCmd = "$PSScriptRoot/build_cudax.ps1 -std $CXX_STANDARD -arch '$CUDA_ARCH' -cmake-options '$CMAKE_OPTIONS'" +Write-Host "Running: $buildCmd" +Invoke-Expression $buildCmd + +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "cudax" +test_preset "CUDA Experimental" "$PRESET" + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_libcudacxx.ps1 b/cccl_upstream/ci/windows/test_libcudacxx.ps1 new file mode 100644 index 00000000..a348475e --- /dev/null +++ b/cccl_upstream/ci/windows/test_libcudacxx.ps1 @@ -0,0 +1,53 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +if ($env:GITHUB_ACTIONS) { + $producerId = & bash "./util/workflow/get_producer_id.sh" + if ($LASTEXITCODE -ne 0) { + throw "Finding the producer job failed (exit code $LASTEXITCODE)" + } + $producerId = "$producerId".Trim() + $artifactName = "z_libcudacxx-test-artifacts-$env:DEVCONTAINER_NAME-$producerId" + Write-Host "Unpacking artifact '$artifactName'" + Invoke-Checked { + & bash "./util/artifacts/download_packed.sh" "$artifactName" "../" + } "Downloading test artifacts failed" +} else { + $buildCmd = "$PSScriptRoot/build_libcudacxx.ps1 -std $CXX_STANDARD -arch '$CUDA_ARCH' -cmake-options '$CMAKE_OPTIONS'" + Write-Host "Running: $buildCmd" + Invoke-Expression $buildCmd +} + +if ($env:GITHUB_ACTIONS) { + test_preset "libcudacxx (CTest)" "libcudacxx-ctest" + $env:LIT_OPTS = "$env:LIT_OPTS -Dtest_executable_mode=replay".Trim() + test_preset "libcudacxx (lit replay)" "libcudacxx-lit" +} else { + test_preset "libcudacxx (CTest)" "libcudacxx-ctest-cpp${CXX_STANDARD}" + test_preset "libcudacxx (lit)" "libcudacxx-lit-cpp${CXX_STANDARD}" +} + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_packaging.ps1 b/cccl_upstream/ci/windows/test_packaging.ps1 new file mode 100644 index 00000000..fae829a4 --- /dev/null +++ b/cccl_upstream/ci/windows/test_packaging.ps1 @@ -0,0 +1,37 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +Import-Module $PSScriptRoot/build_common.psm1 -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +$PRESET = "packaging" +$LOCAL_CMAKE_OPTIONS = "" + +if ($env:GITHUB_SHA) { + $LOCAL_CMAKE_OPTIONS = '"-DCCCL_EXAMPLE_CPM_TAG={0}"' -f $env:GITHUB_SHA +} + +configure_preset "Packaging" $PRESET $LOCAL_CMAKE_OPTIONS +test_preset "Packaging" $PRESET + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/ci/windows/test_thrust.ps1 b/cccl_upstream/ci/windows/test_thrust.ps1 new file mode 100644 index 00000000..0c71f1c0 --- /dev/null +++ b/cccl_upstream/ci/windows/test_thrust.ps1 @@ -0,0 +1,62 @@ +Param( + [Parameter(Mandatory = $false)] + [Alias("std")] + [ValidateNotNullOrEmpty()] + [ValidateSet(17, 20)] + [int]$CXX_STANDARD = 17, + [Parameter(Mandatory = $false)] + [Alias("arch")] + [string]$CUDA_ARCH = "", + [Parameter(Mandatory = $false)] + [Alias("cpu-only")] + [switch]$CPU_ONLY = $false, + [Parameter(Mandatory = $false)] + [Alias("gpu-only")] + [switch]$GPU_ONLY = $false, + [Parameter(Mandatory = $false)] + [Alias("cmake-options")] + [string]$CMAKE_OPTIONS = "" +) + +$ErrorActionPreference = "Stop" + +$CURRENT_PATH = Split-Path $pwd -leaf +If($CURRENT_PATH -ne "ci") { + Write-Host "Moving to ci folder" + pushd "$PSScriptRoot/.." +} + +if ($CPU_ONLY) { + $artifactTag = "test_cpu" + $presets = @("thrust-cpu") +} elseif ($GPU_ONLY) { + $artifactTag = "test_gpu" + $presets = @("thrust-gpu") +} else { + if ($env:GITHUB_ACTIONS) { + throw "Error: test_thrust.ps1 requires -cpu-only or -gpu-only in CI" + } + $artifactTag = "" + $presets = @("thrust-cpu", "thrust-gpu") +} + +if ($env:GITHUB_ACTIONS -and $artifactTag) { + $producerId = (& bash "./util/workflow/get_producer_id.sh").Trim() + $artifactName = "z_thrust-test-artifacts-$env:DEVCONTAINER_NAME-$producerId-$artifactTag" + Write-Host "Unpacking artifact '$artifactName'" + & bash "./util/artifacts/download_packed.sh" "$artifactName" "../" +} else { + $cmd = "$PSScriptRoot/build_thrust.ps1 -std $CXX_STANDARD -arch '$CUDA_ARCH' -cmake-options '$CMAKE_OPTIONS'" + Write-Host "Running: $cmd" + Invoke-Expression $cmd +} + +Import-Module -Name "$PSScriptRoot/build_common.psm1" -ArgumentList @($CXX_STANDARD, $CUDA_ARCH, $CMAKE_OPTIONS) + +foreach ($preset in $presets) { + test_preset "Thrust ($preset)" $preset +} + +If($CURRENT_PATH -ne "ci") { + popd +} diff --git a/cccl_upstream/docs/.gitignore b/cccl_upstream/docs/.gitignore new file mode 100644 index 00000000..28a8ad3f --- /dev/null +++ b/cccl_upstream/docs/.gitignore @@ -0,0 +1,8 @@ +_build +_repo +env +api +*png +cubimg +*/auto_api.rst +*/api/ diff --git a/cccl_upstream/docs/404.html b/cccl_upstream/docs/404.html new file mode 100644 index 00000000..fb0b4e3d --- /dev/null +++ b/cccl_upstream/docs/404.html @@ -0,0 +1,41 @@ + + + + + 404 — CUDA Core Compute Libraries + +
    +
    +
+ + diff --git a/cccl_upstream/docs/404_helper.inc.html b/cccl_upstream/docs/404_helper.inc.html new file mode 100644 index 00000000..83814d7d --- /dev/null +++ b/cccl_upstream/docs/404_helper.inc.html @@ -0,0 +1,63 @@ +
    +
    +
+ diff --git a/cccl_upstream/docs/404_helper.rst b/cccl_upstream/docs/404_helper.rst new file mode 100644 index 00000000..a296dac3 --- /dev/null +++ b/cccl_upstream/docs/404_helper.rst @@ -0,0 +1,9 @@ +:orphan: + +404 +=== + +The page you're seeking could not be found. Here is a list of similar pages: + +.. raw:: html + :file: 404_helper.inc.html diff --git a/cccl_upstream/docs/VERSION.md b/cccl_upstream/docs/VERSION.md new file mode 100644 index 00000000..d70c8f8d --- /dev/null +++ b/cccl_upstream/docs/VERSION.md @@ -0,0 +1 @@ +3.6 diff --git a/cccl_upstream/docs/_ext/__init__.py b/cccl_upstream/docs/_ext/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cccl_upstream/docs/_ext/auto_api_generator.py b/cccl_upstream/docs/_ext/auto_api_generator.py new file mode 100644 index 00000000..11598b0a --- /dev/null +++ b/cccl_upstream/docs/_ext/auto_api_generator.py @@ -0,0 +1,1453 @@ +""" +Sphinx extension to automatically generate API reference pages from Doxygen XML. +Replicates repo-docs' automatic API generation functionality. +""" + +import os +import xml.etree.ElementTree as ET +from pathlib import Path + +from sphinx.application import Sphinx +from sphinx.util import logging + +logger = logging.getLogger(__name__) + +# Symbols whose Doxygen XML produces C++ declarations that breathe/Sphinx +# cannot parse. Skip generating individual API pages for these to avoid +# unfixable build warnings under -W. Names must match the unqualified form +# used by extract_namespace_items (i.e. without the project namespace prefix). +_BREATHE_SKIP_SYMBOLS = frozenset( + { + # cuda::experimental::stf::get_executor_func_t — function-pointer typedef. + # Breathe renders as "pos4(*)(pos4, dim4, dim4)" which Sphinx's C++ parser + # rejects ("Expected end of definition"). + # Listed both qualified (cudax) and unqualified (libcudacxx) since the + # generator uses different naming conventions per project. + "get_executor_func_t", + "cuda::experimental::stf::get_executor_func_t", + # cuda::experimental::stf::partition_fn_t — same issue as get_executor_func_t. + # Function-pointer typedef that Breathe renders in a form Sphinx rejects. + "partition_fn_t", + "cuda::experimental::stf::partition_fn_t", + # cuda::property_with_value — variable template using _CCCL_REQUIRES_EXPR + # with a typename(...) inside the expression. Sphinx cannot parse the + # requires-expression expansion. + "property_with_value", + # cuda::has_property — variable template with _CCCL_REQUIRES_EXPR + # containing a const keyword inside the expression body. + "has_property", + # cuda::experimental::group — variable with complex type expression + # that Sphinx's C++ parser cannot handle. + "cuda::experimental::group", + } +) + + +def extract_param_summary(params): + """Extract a simplified parameter summary for section headers.""" + if not params: + return "" + + # Remove template details and namespaces for brevity + params = params.strip() + if params.startswith("(") and params.endswith(")"): + params = params[1:-1] + + # If empty after removing parentheses + if not params.strip(): + return "" + + # Split by comma (handling nested templates/parentheses) + param_parts = [] + depth = 0 + current = [] + + for char in params: + if char in "<([": + depth += 1 + elif char in ">)]": + depth -= 1 + elif char == "," and depth == 0: + param_parts.append("".join(current).strip()) + current = [] + continue + current.append(char) + + if current: + param_parts.append("".join(current).strip()) + + # Extract parameter names + param_names = [] + + for param in param_parts: + param = param.strip() + + # Special case for execution policy - shorten for readability + if "execution_policy_base" in param: + param_names.append("exec") + else: + # Split by spaces and find the parameter name (typically the last word) + words = param.split() + if words: + # The parameter name is the last word + param_name = words[-1] + # Clean up reference/pointer markers + param_name = param_name.strip("&*,") + + # Just use the parameter name as-is + if param_name: + param_names.append(param_name) + + return ", ".join(param_names) + + +def extract_function_signatures(func_name, refids, xml_dir, namespace=""): + """Extract exact function signatures from Doxygen XML for overloaded functions.""" + signatures = [] + xml_path = Path(xml_dir) + + if not xml_path.exists(): + return signatures + + # Extract the simple function name (without namespace) for comparison + simple_func_name = func_name.split("::")[-1] if "::" in func_name else func_name + + # Parse both namespace and group XML files to get function signatures + # Functions can be defined in either location (often in group_*.xml for thrust/cub) + xml_files = list(xml_path.glob("namespace*.xml")) + list( + xml_path.glob("group*.xml") + ) + + for xml_file in xml_files: + try: + tree = ET.parse(xml_file) + root = tree.getroot() + + # Find all function members with matching refids + for memberdef in root.findall('.//memberdef[@kind="function"]'): + member_refid = memberdef.get("id") + if member_refid in refids: + # Get the function name + name_elem = memberdef.find("name") + if name_elem is not None and name_elem.text == simple_func_name: + # Get the exact args string and definition + argsstring_elem = memberdef.find("argsstring") + definition_elem = memberdef.find("definition") + + if argsstring_elem is not None and argsstring_elem.text: + # Get the full qualified name from definition if available + if definition_elem is not None and definition_elem.text: + # Definition includes namespace and return type + # Extract just the qualified function name + definition = definition_elem.text + # Look for the function name in the definition + if ( + "::" in definition + and simple_func_name in definition + ): + # Extract namespace::function from definition + parts = definition.split() + for part in parts: + if simple_func_name in part and "::" in part: + qualified_name = part + break + else: + # Use the original func_name if it has namespace, otherwise add namespace + qualified_name = ( + func_name + if "::" in func_name + else ( + f"{namespace}::{func_name}" + if namespace + else func_name + ) + ) + else: + # Use the original func_name if it has namespace, otherwise add namespace + qualified_name = ( + func_name + if "::" in func_name + else ( + f"{namespace}::{func_name}" + if namespace + else func_name + ) + ) + else: + # Use the original func_name if it has namespace, otherwise add namespace + qualified_name = ( + func_name + if "::" in func_name + else ( + f"{namespace}::{func_name}" + if namespace + else func_name + ) + ) + + # Build the complete signature for breathe + full_signature = ( + qualified_name + argsstring_elem.text.strip() + ) + # Check if this signature is already in the list (avoid duplicates) + if not any(sig[1] == full_signature for sig in signatures): + signatures.append((member_refid, full_signature)) + except Exception as e: + logger.debug(f"Failed to extract signatures from {xml_file}: {e}") + + return signatures + + +def extract_doxygen_items(xml_dir): + """Extract all items (classes, structs, functions, etc.) from Doxygen XML.""" + items = { + "classes": [], + "structs": [], + "functions": [], + "typedefs": [], + "enums": [], + "variables": [], + "macros": [], + "function_groups": {}, # Group functions by name for overloads + "groups": [], # Doxygen groups + } + + xml_path = Path(xml_dir) + if not xml_path.exists(): + return items + + # Parse index.xml to get all compounds and members + index_file = xml_path / "index.xml" + if not index_file.exists(): + return items + + try: + tree = ET.parse(index_file) + root = tree.getroot() + + # Get the namespace compound (e.g., cub, thrust, cuda::experimental) + namespace_compounds = [] + for compound in root.findall('.//compound[@kind="namespace"]'): + name = compound.find("name").text + # Match primary namespaces and nested namespaces for cudax + if name in ["cub", "thrust", "cuda"] or name.startswith( + "cuda::experimental" + ): + namespace_compounds.append(compound) + + # Extract classes and structs + all_classes = [] + all_structs = [] + + for compound in root.findall('.//compound[@kind="class"]'): + name = compound.find("name").text + refid = compound.get("refid") + + # Skip internal/detail classes + if "detail" in name.lower() or "__" in name: + continue + + all_classes.append((name, refid)) + + for compound in root.findall('.//compound[@kind="struct"]'): + name = compound.find("name").text + refid = compound.get("refid") + + # Skip internal/detail structs + if "detail" in name.lower() or "__" in name: + continue + + all_structs.append((name, refid)) + + # Filter out nested classes/structs when their parent is also documented + # This prevents duplicate declarations in Sphinx + def is_nested_and_parent_exists(name, all_classes, all_structs): + """Check if this is a nested class/struct and its parent is also documented.""" + if "::" not in name: + return False + + # Get the parent name by removing the last component + parent_name = "::".join(name.split("::")[:-1]) + + # Check if parent exists in either classes or structs list + all_items = all_classes + all_structs + for item_name, _ in all_items: + if item_name == parent_name: + return True + return False + + # Filter classes (check against both classes and structs for parents) + for name, refid in all_classes: + if not is_nested_and_parent_exists(name, all_classes, all_structs): + items["classes"].append((name, refid)) + + # Filter structs (check against both classes and structs for parents) + for name, refid in all_structs: + if not is_nested_and_parent_exists(name, all_classes, all_structs): + items["structs"].append((name, refid)) + + # Extract groups and their members + for compound in root.findall('.//compound[@kind="group"]'): + name = compound.find("name").text + refid = compound.get("refid") + items["groups"].append((name, refid)) + + # Also extract typedefs that are members of groups + # We need to get the qualified name from the actual group XML file + for member in compound.findall('member[@kind="typedef"]'): + name_elem = member.find("name") + if name_elem is None: + continue + simple_name = name_elem.text + typedef_refid = member.get("refid") + + # Try to get the qualified name from the group XML file + qualified_name = simple_name # Default to simple name + group_xml_file = xml_path / f"{refid}.xml" + if group_xml_file.exists(): + try: + group_tree = ET.parse(group_xml_file) + group_root = group_tree.getroot() + # Find the typedef with matching refid + typedef_elem = group_root.find( + f'.//memberdef[@id="{typedef_refid}"]' + ) + if typedef_elem is not None: + qualifiedname_elem = typedef_elem.find("qualifiedname") + if ( + qualifiedname_elem is not None + and qualifiedname_elem.text + ): + qualified_name = qualifiedname_elem.text + except Exception: + pass + + items["typedefs"].append((qualified_name, typedef_refid)) + + # Extract documented macros from file compounds. Doxygen records macros as + # file-level "define" members, not namespace members. + seen_macros = set() + for compound in root.findall('.//compound[@kind="file"]'): + refid = compound.get("refid") + if not refid: + continue + + file_xml_file = xml_path / f"{refid}.xml" + if not file_xml_file.exists(): + continue + + try: + file_tree = ET.parse(file_xml_file) + file_root = file_tree.getroot() + for memberdef in file_root.findall('.//memberdef[@kind="define"]'): + macro_refid = memberdef.get("id") + name_elem = memberdef.find("name") + if macro_refid is None or name_elem is None or not name_elem.text: + continue + + has_description = any( + "".join(description.itertext()).strip() + for description in ( + memberdef.find("briefdescription"), + memberdef.find("detaileddescription"), + ) + if description is not None + ) + if not has_description: + continue + + macro_key = (name_elem.text, macro_refid) + if macro_key in seen_macros: + continue + + seen_macros.add(macro_key) + items["macros"].append((name_elem.text, macro_refid)) + except Exception as e: + logger.debug(f"Failed to parse macros from {file_xml_file}: {e}") + + # Extract functions, typedefs, enums, and variables from namespaces + for namespace_compound in namespace_compounds: + namespace_name = namespace_compound.find("name").text + + for member in namespace_compound.findall('member[@kind="function"]'): + name = member.find("name").text + refid = member.get("refid") + # Only include full namespace for nested namespaces (cudax) + # For simple namespaces like 'thrust', 'cub', just use the function name + if namespace_name and "::" in namespace_name: + full_name = f"{namespace_name}::{name}" + else: + full_name = name + items["functions"].append((full_name, refid)) + + # Also track function groups for overloads + if full_name not in items["function_groups"]: + items["function_groups"][full_name] = [] + items["function_groups"][full_name].append(refid) + + for member in namespace_compound.findall('member[@kind="typedef"]'): + name = member.find("name").text + refid = member.get("refid") + # Only include full namespace for nested namespaces + if namespace_name and "::" in namespace_name: + full_name = f"{namespace_name}::{name}" + else: + full_name = name + items["typedefs"].append((full_name, refid)) + + for member in namespace_compound.findall('member[@kind="enum"]'): + name = member.find("name").text + refid = member.get("refid") + # Only include full namespace for nested namespaces + if namespace_name and "::" in namespace_name: + full_name = f"{namespace_name}::{name}" + else: + full_name = name + items["enums"].append((full_name, refid)) + + for member in namespace_compound.findall('member[@kind="variable"]'): + name = member.find("name").text + refid = member.get("refid") + # Only include full namespace for nested namespaces + if namespace_name and "::" in namespace_name: + full_name = f"{namespace_name}::{name}" + else: + full_name = name + items["variables"].append((full_name, refid)) + + except Exception as e: + logger.warning(f"Failed to parse Doxygen XML: {e}") + + return items + + +# Unlike other normal symbols, doxygen treats them as floating in the global namespace +# (which, technically, is correct). As such, they aren't namespaced, and the name of the +# generated files would just be _, which is nearly +# impossible to name in hand-written rst code. +# +# We therefore perform special mangling of the generated file name and prepend this prefix +# to it. +MACRO_FILE_NAME_PREFIX = "macro_" + + +def format_macro_doc_filename(macro_name): + """Create a stable filename for a generated macro page. Macros are not namespace + members so their refid's (and generated pages) are just the name of the file and a + hash. This makes it impossible to refer to macros in hand-written rst code.""" + macro_name = macro_name.casefold().replace(" ", "_") + return f"{MACRO_FILE_NAME_PREFIX}{macro_name}" + + +def extract_doxygen_classes(xml_dir, project_name=None): + """Extract classes categorized by type for category pages.""" + # Only CUB uses these specific categories + if project_name == "cub": + classes = { + "device": [], + "block": [], + "warp": [], + "grid": [], + "iterator": [], + "thread": [], + "utility": [], + } + else: + # Other projects don't need category pages + classes = {} + + # Only categorize for CUB + if project_name == "cub": + items = extract_doxygen_items(xml_dir) + + # Filter out internal implementation details + internal_patterns = [ + "StoreInternal", + "LoadInternal", + "_TempStorage", + "TileDescriptor", + ] + + # Categorize classes and structs + for name, refid in items["classes"] + items["structs"]: + # Skip internal implementation details + if any(pattern in name for pattern in internal_patterns): + continue + + # Remove namespace prefixes for categorization + simple_name = name.split("::")[-1] if "::" in name else name + + # Categorize based on name + if "Device" in simple_name: + classes["device"].append((name, refid)) + elif "Block" in simple_name: + classes["block"].append((name, refid)) + elif "Warp" in simple_name: + classes["warp"].append((name, refid)) + elif "Grid" in simple_name: + classes["grid"].append((name, refid)) + elif "Iterator" in simple_name.lower() or "iterator" in simple_name.lower(): + classes["iterator"].append((name, refid)) + elif any( + x in simple_name + for x in ["Traits", "Type", "Allocator", "Debug", "Caching"] + ): + classes["utility"].append((name, refid)) + + return classes + + +def generate_api_page(category, classes, project_name): + """Generate RST content for an API category page.""" + + category_titles = { + "device": "Device-wide Primitives", + "block": "Block-wide Primitives", + "warp": "Warp-wide Primitives", + "grid": "Grid-level Primitives", + "iterator": "Iterator Utilities", + "thread": "Thread-level Primitives", + "utility": "Utility Components", + } + + content = [] + content.append(category_titles.get(category, f"{category.title()} API")) + content.append("=" * len(content[0])) + content.append("") + content.append(".. contents:: Table of Contents") + content.append(" :local:") + content.append(" :depth: 2") + content.append("") + + # Sort classes by name + classes.sort(key=lambda x: x[0]) + + for class_name, refid in classes: + # Use the full name including namespace for display + display_name = class_name + + content.append(display_name) + content.append("-" * len(display_name)) + content.append("") + + # Check if this is a struct by looking at the refid + # Doxygen uses 'struct' prefix in the refid for structs + directive = "doxygenstruct" if refid.startswith("struct") else "doxygenclass" + + # Use the full qualified name including namespace + content.append(f".. {directive}:: {class_name}") + + content.append(f" :project: {project_name}") + content.append(" :members:") + content.append(" :undoc-members:") + content.append("") + + return "\n".join(content) + + +def generate_group_index_page(group_name, group_refid, project_name, xml_dir, api_dir): + """Generate RST content for a Doxygen group index page.""" + content = [] + + # Add marker comment for auto-generated files + content.append(".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT") + content.append("") + content.append(":orphan:") + content.append("") + + # Parse the group XML to get more details + group_xml_file = Path(xml_dir) / f"{group_refid}.xml" + title = group_name + members = [] + brief_description = "" + + if group_xml_file.exists(): + try: + tree = ET.parse(group_xml_file) + root = tree.getroot() + compounddef = root.find('.//compounddef[@kind="group"]') + + if compounddef is not None: + # Get the title + title_elem = compounddef.find("title") + if title_elem is not None and title_elem.text: + title = title_elem.text + + # Get brief description if available + brief_elem = compounddef.find("briefdescription") + if brief_elem is not None: + # Extract text from brief description + brief_text = "".join(brief_elem.itertext()).strip() + if brief_text: + brief_description = brief_text + + # Get all inner classes/structs + for innerclass in compounddef.findall("innerclass"): + class_refid = innerclass.get("refid") + class_name = innerclass.text + members.append(("class", class_name, class_refid)) + + # Get all member functions, typedefs, variables, etc. + for sectiondef in compounddef.findall("sectiondef"): + for memberdef in sectiondef.findall("memberdef"): + member_name = memberdef.find("name") + if member_name is None: + continue + member_kind = memberdef.get("kind") + member_refid = memberdef.get("id") + member_filename = ( + format_macro_doc_filename(member_name.text) + if member_kind == "define" + else member_refid + ) + member_rst_file = Path(api_dir) / f"{member_filename}.rst" + # only add refids for exists pages. + # Refids corresponding to overloads + # do not have associated RST file + if not member_rst_file.exists(): + continue + members.append((member_kind, member_name.text, member_filename)) + except Exception as e: + logger.warning(f"Failed to parse group XML {group_xml_file}: {e}") + + # Add title + content.append(title) + content.append("=" * len(title)) + content.append("") + + # Add brief description if available + if brief_description: + content.append(brief_description) + content.append("") + + # Do NOT use doxygengroup directive to avoid duplicate declarations + # Instead, just provide a simple page with links to members + + # Provide a list of links instead of a toctree to avoid duplicate-toctree warnings. + if members: + content.append("Members") + content.append("-------") + content.append("") + for member_kind, member_name, member_refid in members: + content.append( + format_doc_reference(member_name, member_refid, "", as_list_item=True) + ) + content.append("") + + return "\n".join(content) + + +def generate_individual_api_page(class_name, refid, project_name): + """Generate RST content for a single class/struct API page.""" + content = [] + + # Add marker comment for auto-generated files + content.append(".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT") + content.append("") + content.append(":orphan:") + content.append("") + + # Add title + content.append(class_name) + content.append("=" * len(class_name)) + content.append("") + + # Check if this is a struct by looking at the refid + directive = "doxygenstruct" if refid.startswith("struct") else "doxygenclass" + + # Add the doxygen directive + content.append(f".. {directive}:: {class_name}") + content.append(f" :project: {project_name}") + content.append(" :members:") + content.append(" :undoc-members:") + content.append("") + + return "\n".join(content) + + +def check_function_in_namespace(member_name, xml_dir, namespace): + """Check if a function is defined in the namespace XML (not just referenced).""" + namespace_xml = os.path.join(xml_dir, f"namespace{namespace}.xml") + if not os.path.exists(namespace_xml): + return False + + try: + tree = ET.parse(namespace_xml) + root = tree.getroot() + + # Look for actual function definitions, not just references + for memberdef in root.findall('.//memberdef[@kind="function"]'): + name_elem = memberdef.find("name") + if name_elem is not None and name_elem.text == member_name: + # Check if it has a definition (not just a reference) + definition = memberdef.find("definition") + if definition is not None and definition.text: + return True + return False + except Exception: + return False + + +def generate_member_api_page( + member_name, + member_type, + project_name, + refid=None, + overload_refids=None, + xml_dir=None, +): + """Generate RST content for a single function/typedef/enum/variable/macro API page.""" + content = [] + + # Add marker comment for auto-generated files + content.append(".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT") + content.append("") + content.append(":orphan:") + content.append("") + + # Map member types to Doxygen directives + directive_map = { + "function": "doxygenfunction", + "typedef": "doxygentypedef", + "enum": "doxygenenum", + "variable": "doxygenvariable", + "macro": "doxygendefine", + } + + directive = directive_map.get(member_type, "doxygenfunction") + + # For thrust and cub, we need to use the namespace-qualified name + # For cudax, the member_name already includes the namespace + if member_type == "macro": + qualified_name = member_name + elif project_name in ["thrust", "cub"]: + # If the member_name doesn't already include the namespace, add it + if "::" not in member_name: + qualified_name = f"{project_name}::{member_name}" + else: + qualified_name = member_name + else: + qualified_name = member_name + + # Add title + content.append(f"{qualified_name}") + content.append("=" * (len(qualified_name) + 4)) + content.append("") + + if member_type == "function" and overload_refids: + # Check if functions are in a group + is_group_function = False + group_name = None + + if overload_refids and "_1" in overload_refids[0]: + parts = overload_refids[0].split("_1") + if parts[0].startswith("group__"): + is_group_function = True + # Get the group refid (e.g., 'group__stream__compaction') + group_refid = parts[0] + + # Look up the actual group name from index.xml + group_name = None + if xml_dir: + index_xml = os.path.join(xml_dir, "index.xml") + if os.path.exists(index_xml): + try: + tree = ET.parse(index_xml) + root = tree.getroot() + # Find the compound with this refid + compound = root.find(f'.//compound[@refid="{group_refid}"]') + if compound is not None: + name_elem = compound.find("name") + if name_elem is not None: + group_name = name_elem.text + except Exception: + pass + + # Fallback: remove 'group__' prefix if lookup fails + if not group_name: + group_name = group_refid[7:] + + if is_group_function and group_name: + # For group functions with overloads, we need to handle them specially + if len(overload_refids) > 1 and xml_dir: + # Extract signatures for all overloads + signatures = extract_function_signatures( + member_name, overload_refids, xml_dir, namespace=project_name + ) + + if signatures: + content.append("Overloads") + content.append("---------") + content.append("") + + for idx, (refid, full_sig) in enumerate(signatures, 1): + # Extract just the parameter list from the full signature + simple_name = ( + member_name.split("::")[-1] + if "::" in member_name + else member_name + ) + if simple_name in full_sig: + # Use find instead of rfind to get the first occurrence + sig_idx = full_sig.find(simple_name) + if sig_idx != -1: + params = full_sig[sig_idx + len(simple_name) :].strip() + # Handle trailing noexcept specifier + # Look for the closing parenthesis of the parameter list + paren_count = 0 + param_end = -1 + for i, char in enumerate(params): + if char == "(": + paren_count += 1 + elif char == ")": + paren_count -= 1 + if paren_count == 0: + param_end = i + 1 + break + if param_end > 0: + # Keep only the parameter list (up to and including the closing parenthesis) + params = params[:param_end] + + # Create a simplified signature for the section header + param_summary = extract_param_summary(params) + + # Add a section header for this overload + content.append(f"``{simple_name}({param_summary})``") + content.append( + "^" * (len(simple_name) + len(param_summary) + 6) + ) + content.append("") + + # Use doxygenfunction with the specific parameter signature + content.append( + f".. doxygenfunction:: {qualified_name}{params}" + ) + content.append(f" :project: {project_name}") + content.append("") + else: + # Fallback to using doxygengroup if we can't extract signatures + content.append(f".. doxygengroup:: {group_name}") + content.append(f" :project: {project_name}") + content.append(" :content-only:") + content.append("") + else: + # Single overload from group - but there might be other overloads in the namespace + # Extract the signature to be specific + if xml_dir: + signatures = extract_function_signatures( + member_name, overload_refids, xml_dir, namespace=project_name + ) + if signatures and len(signatures) == 1: + refid, full_sig = signatures[0] + simple_name = ( + member_name.split("::")[-1] + if "::" in member_name + else member_name + ) + if simple_name in full_sig: + # Use find instead of rfind to get the first occurrence + sig_idx = full_sig.find(simple_name) + if sig_idx != -1: + params = full_sig[sig_idx + len(simple_name) :].strip() + # Handle trailing noexcept specifier + # Look for the closing parenthesis of the parameter list + paren_count = 0 + param_end = -1 + for i, char in enumerate(params): + if char == "(": + paren_count += 1 + elif char == ")": + paren_count -= 1 + if paren_count == 0: + param_end = i + 1 + break + if param_end > 0: + # Keep only the parameter list (up to and including the closing parenthesis) + params = params[:param_end] + content.append( + f".. doxygenfunction:: {qualified_name}{params}" + ) + content.append(f" :project: {project_name}") + content.append("") + else: + # Fallback to simple + content.append(f".. doxygenfunction:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + else: + # Fallback to simple + content.append(f".. doxygenfunction:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + else: + # Fallback to simple + content.append(f".. doxygenfunction:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + else: + # No xml_dir, use simple + content.append(f".. doxygenfunction:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + elif len(overload_refids) > 1 and xml_dir: + # For functions with multiple overloads in namespace, extract signatures + # For cudax, member_name already includes namespace, for others use project_name + if "::" in member_name: + # Extract namespace from the qualified name for cudax + namespace_parts = member_name.split("::")[:-1] + namespace_name = ( + "::".join(namespace_parts) if namespace_parts else project_name + ) + else: + namespace_name = project_name + signatures = extract_function_signatures( + member_name, overload_refids, xml_dir, namespace=namespace_name + ) + + if signatures: + content.append("Overloads") + content.append("---------") + content.append("") + + for idx, (refid, full_sig) in enumerate(signatures, 1): + # Extract just the parameter list from the full signature + # Look for the simple function name (without namespace) in the signature + simple_name = ( + member_name.split("::")[-1] + if "::" in member_name + else member_name + ) + if simple_name in full_sig: + sig_idx = full_sig.rfind(simple_name) + if sig_idx != -1: + params = full_sig[sig_idx + len(simple_name) :].strip() + + # Create a simplified signature for the section header + # Extract key parameter types for identification + param_summary = extract_param_summary(params) + + # Add a section header for this overload + # Use simple name for readability in headers + content.append(f"``{simple_name}({param_summary})``") + content.append( + "^" * (len(simple_name) + len(param_summary) + 6) + ) + content.append("") + + # Use doxygenfunction with the specific parameter signature and qualified name + content.append( + f".. doxygenfunction:: {qualified_name}{params}" + ) + content.append(f" :project: {project_name}") + content.append(" :no-link:") + content.append("") + else: + # Fallback to simple directive with qualified name + content.append(f".. {directive}:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + else: + # Single function with qualified name + content.append(f".. {directive}:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + elif member_type == "function": + # For single functions or when we don't have xml_dir, use qualified name + content.append(f".. {directive}:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + else: + # For other types, use the qualified name + content.append(f".. {directive}:: {qualified_name}") + content.append(f" :project: {project_name}") + content.append("") + + return "\n".join(content) + + +def generate_category_index(category, class_list, project_name): + """Generate an index page for a category with links to individual class pages.""" + category_titles = { + "device": "Device-wide Primitives", + "block": "Block-wide Primitives", + "warp": "Warp-wide Primitives", + "grid": "Grid-level Primitives", + "iterator": "Iterator Utilities", + "thread": "Thread-level Primitives", + "utility": "Utility Components", + } + + content = [] + + # Add marker comment for auto-generated files + content.append(".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT") + content.append("") + content.append(":orphan:") + content.append("") + + title = category_titles.get(category, f"{category.title()} API") + content.append(title) + content.append("=" * len(title)) + content.append("") + + # Add toctree for all classes in this category + content.append(".. toctree::") + content.append(" :maxdepth: 1") + content.append(" :hidden:") + content.append("") + + # Sort classes by name + class_list.sort(key=lambda x: x[0]) + + for class_name, refid in class_list: + # Generate filename from refid (e.g., structcub_1_1DeviceAdjacentDifference) + filename = refid + content.append(f" {filename}") + + content.append("") + content.append(".. list-table::") + content.append(" :widths: 50 50") + content.append(" :header-rows: 1") + content.append("") + content.append(" * - Class/Struct") + content.append(" - Description") + + for class_name, refid in class_list: + filename = refid + # Use format_doc_reference but without the list item marker + doc_ref = format_doc_reference(class_name, filename, "", as_list_item=False) + content.append(f" * - {doc_ref}") + content.append(" - ") # Description would go here if available + + return "\n".join(content) + + +def clean_template_name(name): + """Remove spaces around template parameters to avoid Sphinx parsing issues.""" + # Remove spaces after '<' and before '>' + cleaned = name.replace("< ", "<").replace(" >", ">") + # Handle pointer types - remove space before * + cleaned = cleaned.replace(" *", "*") + # Handle spaces before commas in template parameters + while " ," in cleaned: + cleaned = cleaned.replace(" ,", ",") + # Handle spaces after commas in template parameters + while ", " in cleaned: + cleaned = cleaned.replace(", ", ",") + # Handle spaces after :: in nested namespaces + cleaned = cleaned.replace(":: ", "::") + return cleaned + + +def format_doc_reference(name, refid, doc_prefix="", as_list_item=True): + """Format a documentation reference, handling template specializations.""" + clean_name = clean_template_name(name) + + # Build the reference path + ref_path = f"{doc_prefix}{refid}" if doc_prefix else refid + + # Check if the name contains any angle brackets (template) + if "<" in clean_name: + # For any template specialization, just use the file reference without display name + # This avoids RST parsing issues with angle brackets in the display name + doc_ref = f":doc:`{ref_path}`" + else: + # For non-templates, use the full name as display + doc_ref = f":doc:`{clean_name} <{ref_path}>`" + + # Return with or without list item marker + return f"* {doc_ref}" if as_list_item else doc_ref + + +def generate_namespace_api_page(project_name, items, title=None, doc_prefix=""): + """Generate a comprehensive namespace API reference page.""" + content = [] + + # Add marker comment for auto-generated files + content.append(".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT") + content.append("") + + # Determine namespace name + namespace_name = project_name # e.g., 'cub', 'thrust', etc. + + # Title - use provided title or default + if not title: + title = f"{project_name.upper()} API Reference" + + content.append(title) + content.append("=" * len(title)) + content.append("") + + # Add namespace description + namespace_title = f"Namespace ``{namespace_name}``" + content.append(namespace_title) + # Make underline slightly longer to avoid "too short" warnings + content.append("-" * (len(namespace_title) + 2)) + content.append("") + + # Filter out internal implementation details + internal_patterns = [ + "StoreInternal", + "LoadInternal", + "_TempStorage", + "TileDescriptor", + ] + + # Classes section + filtered_classes = [ + (name, refid) + for name, refid in items["classes"] + if not any(pattern in name for pattern in internal_patterns) + ] + if filtered_classes: + content.append("Classes") + content.append("~~~~~~~") + content.append("") + + # Sort classes alphabetically + filtered_classes.sort(key=lambda x: x[0].lower()) + for name, refid in filtered_classes: + content.append(format_doc_reference(name, refid, doc_prefix)) + content.append("") + + # Structs section + filtered_structs = [ + (name, refid) + for name, refid in items["structs"] + if not any(pattern in name for pattern in internal_patterns) + ] + if filtered_structs: + content.append("Structs") + content.append("~~~~~~~") + content.append("") + + # Sort structs alphabetically + filtered_structs.sort(key=lambda x: x[0].lower()) + for name, refid in filtered_structs: + content.append(format_doc_reference(name, refid, doc_prefix)) + content.append("") + + # Functions section + if items.get("function_groups"): + content.append("Functions") + content.append("~~~~~~~~~") + content.append("") + + # Sort functions alphabetically by name + sorted_functions = sorted( + items["function_groups"].keys(), key=lambda x: x.lower() + ) + for func_name in sorted_functions: + # Use the first refid for the link + first_refid = items["function_groups"][func_name][0] + content.append(format_doc_reference(func_name, first_refid, doc_prefix)) + content.append("") + + # Typedefs section + if items["typedefs"]: + content.append("Type Definitions") + content.append("~~~~~~~~~~~~~~~~") + content.append("") + + # Sort typedefs alphabetically + items["typedefs"].sort(key=lambda x: x[0].lower()) + for name, refid in items["typedefs"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info( + f"Skipping typedef reference {name} (in _BREATHE_SKIP_SYMBOLS)" + ) + continue + content.append(format_doc_reference(name, refid, doc_prefix)) + content.append("") + + # Enums section + if items["enums"]: + content.append("Enumerations") + content.append("~~~~~~~~~~~~") + content.append("") + + # Sort enums alphabetically + items["enums"].sort(key=lambda x: x[0].lower()) + for name, refid in items["enums"]: + content.append(format_doc_reference(name, refid, doc_prefix)) + content.append("") + + # Variables section + if items["variables"]: + content.append("Variables") + content.append("~~~~~~~~~") + content.append("") + + # Sort variables alphabetically + items["variables"].sort(key=lambda x: x[0].lower()) + for name, refid in items["variables"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info( + f"Skipping variable reference {name} (in _BREATHE_SKIP_SYMBOLS)" + ) + continue + content.append(format_doc_reference(name, refid, doc_prefix)) + content.append("") + + # Macros section + if items["macros"]: + content.append("Macros") + content.append("~~~~~~") + content.append("") + + # Sort macros alphabetically + items["macros"].sort(key=lambda x: x[0].lower()) + for name, refid in items["macros"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info( + f"Skipping macro reference {name} (in _BREATHE_SKIP_SYMBOLS)" + ) + continue + filename = format_macro_doc_filename(name) + content.append(format_doc_reference(name, filename, doc_prefix)) + content.append("") + + # Keep API reference pages as leaf nodes in sidebar navigation. + # Symbol/group/category pages are still discoverable on-page and via search. + + return "\n".join(content) + + +def generate_api_docs(app, config): + """Generate API documentation pages during Sphinx build.""" + + # Only generate for projects with breathe configuration + if not hasattr(config, "breathe_projects"): + return + + for project_name, xml_dir in config.breathe_projects.items(): + # Skip if XML directory doesn't exist + if not os.path.exists(xml_dir): + continue + + # Extract all items from Doxygen XML + items = extract_doxygen_items(xml_dir) + + # Also extract categorized classes for category pages + classes = extract_doxygen_classes(xml_dir, project_name) + + # Determine output directory based on project + api_dir = None + if project_name == "cub": + api_dir = Path(app.srcdir) / "cub" / "api" + elif project_name == "thrust": + api_dir = Path(app.srcdir) / "thrust" / "api" + elif project_name == "libcudacxx": + api_dir = Path(app.srcdir) / "libcudacxx" / "api" + elif project_name == "cudax": + api_dir = Path(app.srcdir) / "cudax" / "api" + + if not api_dir: + continue + + # Clean up auto-generated files first + # Remove all auto-generated .rst files (those matching certain patterns) + if api_dir.exists(): + logger.info(f"Cleaning up auto-generated files in {api_dir}") + # Remove files matching Doxygen reference patterns + for pattern in [ + "class*.rst", + "struct*.rst", + "group*.rst", + "namespace*.rst", + f"{MACRO_FILE_NAME_PREFIX}*.rst", + ]: + for file in api_dir.glob(pattern): + file.unlink() + logger.debug(f"Removed {file}") + + # Also remove category files that are auto-generated + for category in [ + "device", + "block", + "warp", + "grid", + "iterator", + "thread", + "utility", + ]: + category_file = api_dir / f"{category}.rst" + if category_file.exists(): + # Check if it's auto-generated by looking for our marker + try: + with open(category_file, "r") as f: + first_line = f.readline() + if "AUTO-GENERATED" in first_line: + category_file.unlink() + logger.debug(f"Removed auto-generated {category_file}") + except Exception: + pass + + # Remove index.rst if it's auto-generated + index_file = api_dir / "index.rst" + if index_file.exists(): + try: + with open(index_file, "r") as f: + first_line = f.readline() + if "AUTO-GENERATED" in first_line: + index_file.unlink() + logger.debug(f"Removed auto-generated {index_file}") + except Exception: + pass + + # Note: We no longer generate or clean up auto_api.rst files + + # Create API directory if it doesn't exist + api_dir.mkdir(parents=True, exist_ok=True) + + # Generate individual pages for each class/struct + # Skip internal implementation details that cause template warnings + internal_patterns = [ + "StoreInternal", + "LoadInternal", + "_TempStorage", + "TileDescriptor", + ] + + for name, refid in items["classes"] + items["structs"]: + # Skip internal implementation details + if any(pattern in name for pattern in internal_patterns): + logger.debug(f"Skipping internal implementation detail: {name}") + continue + + # Note: We generate individual pages for all classes/structs, + # even if they're in groups, so that references work correctly + + # Generate individual page + content = generate_individual_api_page(name, refid, project_name) + output_file = api_dir / f"{refid}.rst" + + # Write the individual class page + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated API page: {output_file}") + + # Generate individual pages for functions (one per unique function name) + # Use function_groups to handle overloads + function_groups = items.get("function_groups", {}) + for func_name in function_groups: + # Use the first refid as the filename for consistency + first_refid = function_groups[func_name][0] + content = generate_member_api_page( + func_name, + "function", + project_name, + refid=first_refid, + overload_refids=function_groups[func_name], + xml_dir=xml_dir, + ) + output_file = api_dir / f"{first_refid}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated function API page: {output_file}") + + # Generate individual pages for typedefs + for name, refid in items["typedefs"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info(f"Skipping typedef {name} (in _BREATHE_SKIP_SYMBOLS)") + continue + content = generate_member_api_page(name, "typedef", project_name, refid) + output_file = api_dir / f"{refid}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated typedef API page: {output_file}") + + # Generate individual pages for enums + for name, refid in items["enums"]: + content = generate_member_api_page(name, "enum", project_name, refid) + output_file = api_dir / f"{refid}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated enum API page: {output_file}") + + # Generate individual pages for variables + for name, refid in items["variables"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info(f"Skipping variable {name} (in _BREATHE_SKIP_SYMBOLS)") + continue + content = generate_member_api_page(name, "variable", project_name, refid) + output_file = api_dir / f"{refid}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated variable API page: {output_file}") + + # Generate individual pages for macros + for name, refid in items["macros"]: + if name in _BREATHE_SKIP_SYMBOLS: + logger.info(f"Skipping macro {name} (in _BREATHE_SKIP_SYMBOLS)") + continue + content = generate_member_api_page(name, "macro", project_name, refid) + output_file = api_dir / f"{format_macro_doc_filename(name)}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated macro API page: {output_file}") + + # Generate group index pages + for group_name, group_refid in items["groups"]: + content = generate_group_index_page( + group_name, group_refid, project_name, xml_dir, api_dir + ) + output_file = api_dir / f"{group_refid}.rst" + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated group index page: {output_file}") + + # Generate the main namespace API reference page for api/index.rst + namespace_content = generate_namespace_api_page(project_name, items) + namespace_file = api_dir / "index.rst" + with open(namespace_file, "w") as f: + f.write(namespace_content) + logger.info(f"Generated namespace API reference: {namespace_file}") + + # Note: We no longer generate auto_api.rst as api/index.rst serves the same purpose + + # Generate category index pages (for backward compatibility) + # Create stub files for all categories to avoid toctree warnings + for category, class_list in classes.items(): + output_file = api_dir / f"{category}.rst" + + if class_list: + content = generate_category_index(category, class_list, project_name) + else: + # Create a minimal stub file for empty categories + category_titles = { + "device": "Device-wide Primitives", + "block": "Block-wide Primitives", + "warp": "Warp-wide Primitives", + "grid": "Grid-wide Primitives", + "iterator": "Iterator Utilities", + "thread": "Thread-level Primitives", + "utility": "Utility Components", + } + title = category_titles.get(category, category.title()) + content = f""".. AUTO-GENERATED by auto_api_generator.py - DO NOT EDIT + +:orphan: + +{title} +{"=" * len(title)} + +.. note:: + No items in this category. +""" + + # Write the category index + with open(output_file, "w") as f: + f.write(content) + logger.info(f"Generated category index: {output_file}") + + +def setup(app: Sphinx): + """Setup the extension.""" + app.connect("config-inited", generate_api_docs) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/cccl_upstream/docs/_static/deduplicate_toc.js b/cccl_upstream/docs/_static/deduplicate_toc.js new file mode 100644 index 00000000..1bcfa8a6 --- /dev/null +++ b/cccl_upstream/docs/_static/deduplicate_toc.js @@ -0,0 +1,32 @@ +// Clean up the "On this page" sidebar for C++ API pages. +// +// Two problems caused by Breathe's per-overload anchor generation: +// 1. toc-h4 entries: Breathe adds a bare redundant "transform()" child anchor under each +// overload's section heading. Always remove them. +// 2. Duplicate toc-h3 entries: when all overloads share the same display name +// (e.g. "ExclusiveSum()"), keep only the first occurrence. +document.addEventListener('DOMContentLoaded', function() { + var tocNav = document.getElementById('pst-page-toc-nav'); + if (!tocNav) + return; + + tocNav.querySelectorAll('li.toc-h4').forEach(function(li) { + var label = li.textContent.trim(); + if (label.endsWith('()')) { + li.remove(); + } + }); + + var seen = new Set(); + tocNav.querySelectorAll('li.toc-h3').forEach(function(li) { + var label = li.textContent.trim(); + if (!label.endsWith(')')) { + return; + } + if (seen.has(label)) { + li.remove(); + } else { + seen.add(label); + } + }); +}); diff --git a/cccl_upstream/docs/_static/nvidia-logo.png b/cccl_upstream/docs/_static/nvidia-logo.png new file mode 100644 index 00000000..1779ad93 Binary files /dev/null and b/cccl_upstream/docs/_static/nvidia-logo.png differ diff --git a/cccl_upstream/docs/_static/search_custom.css b/cccl_upstream/docs/_static/search_custom.css new file mode 100644 index 00000000..a503afc0 --- /dev/null +++ b/cccl_upstream/docs/_static/search_custom.css @@ -0,0 +1,41 @@ +.cccl-search-breadcrumbs { + display: flex; + flex-wrap: wrap; + font-size: 0.72em; + line-height: normal; + list-style: none; + padding: 0; +} + +.cccl-search-breadcrumbs .breadcrumb-item { + align-items: center; + display: flex; + font-weight: 700; + margin: 0; + padding: 0; + white-space: nowrap; +} + +.cccl-search-breadcrumbs .breadcrumb-item a { + color: var(--pst-color-text-muted); + margin: 0.1875rem; + overflow-x: hidden; + text-decoration: none; + text-overflow: ellipsis; +} + +.cccl-search-breadcrumbs .breadcrumb-item a:hover { + color: var(--pst-color-link-hover); + text-decoration: underline; + text-decoration-skip-ink: none; + text-decoration-thickness: max(3px, 0.1875rem, 0.12em); + text-underline-offset: 0.1578em; +} + +.cccl-search-breadcrumbs .breadcrumb-item + .breadcrumb-item::before { + color: var(--pst-color-text-muted); + content: var(--pst-breadcrumb-divider); + font: var(--fa-font-solid); + font-size: 0.8rem; + padding: 0 0.5rem; +} diff --git a/cccl_upstream/docs/_static/search_postprocess.js b/cccl_upstream/docs/_static/search_postprocess.js new file mode 100644 index 00000000..b8bae1d5 --- /dev/null +++ b/cccl_upstream/docs/_static/search_postprocess.js @@ -0,0 +1,265 @@ +"use strict"; + +(function () { + const maxBreadcrumbResults = 10; + + const decodeEntities = (value) => + String(value || "") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'|'/g, "'") + .replace(/ /g, " ") + .replace(/…/g, "..."); + + const getDocLinkSuffix = () => + (typeof DOCUMENTATION_OPTIONS !== "undefined" && + DOCUMENTATION_OPTIONS.LINK_SUFFIX) || + ".html"; + + const pageInfoCache = new Map(); + + const getDocTitle = (docName) => { + if ( + typeof Search === "undefined" || + !Search._index || + !Array.isArray(Search._index.docnames) || + !Array.isArray(Search._index.titles) + ) { + return null; + } + + const index = Search._index.docnames.indexOf(docName); + return index >= 0 ? Search._index.titles[index] : null; + }; + + const getResultDocNameFromHref = (href) => { + if (!href) { + return null; + } + + const withoutAnchor = String(href).split("#", 1)[0]; + const linkSuffix = getDocLinkSuffix(); + const suffixIndex = withoutAnchor.lastIndexOf(linkSuffix); + if (suffixIndex < 0) { + return null; + } + + let docName = withoutAnchor.slice(0, suffixIndex); + if (docName.startsWith("./")) { + docName = docName.slice(2); + } + + const contentRoot = + document?.documentElement?.dataset?.content_root || ""; + if (contentRoot && docName.startsWith(contentRoot)) { + docName = docName.slice(contentRoot.length); + } + + return docName.replace(/^\/+/, ""); + }; + + const getPageInfo = async (docName) => { + if (pageInfoCache.has(docName)) { + return pageInfoCache.get(docName); + } + + const infoPromise = (async () => { + const pageUrl = `${docName}${getDocLinkSuffix()}`; + const response = await fetch(pageUrl); + const html = await response.text(); + const parsed = new DOMParser().parseFromString(html, "text/html"); + + const pageHeading = parsed.querySelector("h1"); + const breadcrumbLinks = Array.from( + parsed.querySelectorAll(".breadcrumb-item a.nav-link"), + ); + const breadcrumbs = breadcrumbLinks + .map((breadcrumbLink) => { + const rawHref = breadcrumbLink.getAttribute("href"); + if (!rawHref) { + return null; + } + + return { + href: new URL(rawHref, response.url).href, + title: breadcrumbLink.textContent?.trim() || null, + }; + }) + .filter((breadcrumb) => breadcrumb && breadcrumb.title); + + return { + pageTitle: + pageHeading?.textContent?.replace(/#\s*$/, "").trim() || + getDocTitle(docName) || + null, + breadcrumbs, + }; + })().catch(() => null); + + pageInfoCache.set(docName, infoPromise); + return infoPromise; + }; + + const addBreadcrumbTrail = async (listItem) => { + if ( + !listItem || + listItem.dataset.ccclBreadcrumbsAttached === "true" || + listItem.dataset.ccclBreadcrumbsPending === "true" + ) { + return; + } + listItem.dataset.ccclBreadcrumbsPending = "true"; + + const primaryLink = listItem.querySelector("a"); + if (!primaryLink) { + delete listItem.dataset.ccclBreadcrumbsPending; + return; + } + + const primaryTitle = primaryLink.textContent?.trim() || ""; + const href = primaryLink.getAttribute("href"); + const docName = getResultDocNameFromHref(href); + if (!docName) { + delete listItem.dataset.ccclBreadcrumbsPending; + return; + } + + const pageInfo = await getPageInfo(docName); + if (!pageInfo) { + delete listItem.dataset.ccclBreadcrumbsPending; + return; + } + + const pageTitle = pageInfo.pageTitle || getDocTitle(docName); + const breadcrumbs = [...(pageInfo.breadcrumbs || [])]; + if (pageTitle && primaryTitle && pageTitle !== primaryTitle) { + breadcrumbs.push({ + href: `${docName}${getDocLinkSuffix()}`, + title: pageTitle, + }); + } + + if (breadcrumbs.length === 0) { + delete listItem.dataset.ccclBreadcrumbsPending; + return; + } + + const breadcrumbContainer = document.createElement("div"); + breadcrumbContainer.className = "cccl-search-breadcrumbs"; + + breadcrumbs.forEach((breadcrumb) => { + const breadcrumbItem = document.createElement("span"); + breadcrumbItem.className = "breadcrumb-item"; + const breadcrumbLink = document.createElement("a"); + breadcrumbLink.href = breadcrumb.href; + breadcrumbLink.textContent = breadcrumb.title; + breadcrumbItem.appendChild(breadcrumbLink); + breadcrumbContainer.appendChild(breadcrumbItem); + }); + + listItem.insertBefore(breadcrumbContainer, primaryLink.nextSibling); + listItem.dataset.ccclBreadcrumbsAttached = "true"; + delete listItem.dataset.ccclBreadcrumbsPending; + }; + + const installResultDecorator = () => { + if ( + typeof Search === "undefined" || + Search.__ccclResultDecoratorInstalled || + typeof MutationObserver === "undefined" + ) { + return; + } + + const originalPerformSearch = Search.performSearch; + Search.performSearch = (...args) => { + const result = originalPerformSearch(...args); + const output = Search.output; + if (!output) { + return result; + } + + const decorateTopResults = () => { + Array.from(output.querySelectorAll("li")) + .slice(0, maxBreadcrumbResults) + .forEach(addBreadcrumbTrail); + }; + + if (Search.__ccclResultsObserver) { + Search.__ccclResultsObserver.disconnect(); + } + + decorateTopResults(); + + const observer = new MutationObserver((mutations) => { + decorateTopResults(); + }); + + observer.observe(output, { childList: true, subtree: true }); + Search.__ccclResultsObserver = observer; + Search.__ccclResultDecoratorInstalled = true; + return result; + }; + }; + + const installPostprocess = () => { + if (typeof Search === "undefined" || Search.__ccclDedupInstalled) { + return; + } + + const originalPerformSearch = Search._performSearch; + Search._performSearch = (...args) => { + const results = originalPerformSearch(...args); + + // Sphinx keeps results in low->high score order and displays via pop(). + // Walk from the end so we see the best-ranked result first, but prefer + // canonical page links without anchors when collapsing duplicates. + const chosen = new Map(); + for (let i = results.length - 1; i >= 0; --i) { + const result = results[i]; + const title = String(result[1] || "").toLowerCase(); + const filename = String(result[5] || ""); + const key = `${filename}\0${title}`; + const anchor = String(result[2] || ""); + const existing = chosen.get(key); + if (!existing) { + chosen.set(key, result); + continue; + } + + const existingAnchor = String(existing[2] || ""); + const prefersCurrent = existingAnchor && !anchor; + if (prefersCurrent) { + chosen.set(key, result); + } + } + + const deduped = []; + const emitted = new Set(); + for (let i = 0; i < results.length; ++i) { + const result = results[i]; + const title = String(result[1] || "").toLowerCase(); + const filename = String(result[5] || ""); + const key = `${filename}\0${title}`; + if (emitted.has(key)) { + continue; + } + const winner = chosen.get(key); + if (winner) { + winner[1] = decodeEntities(winner[1]); + winner[3] = decodeEntities(winner[3]); + deduped.push(winner); + emitted.add(key); + } + } + return deduped; + }; + + Search.__ccclDedupInstalled = true; + }; + + installPostprocess(); + installResultDecorator(); +})(); diff --git a/cccl_upstream/docs/_static/search_scorer.js b/cccl_upstream/docs/_static/search_scorer.js new file mode 100644 index 00000000..81612e4f --- /dev/null +++ b/cccl_upstream/docs/_static/search_scorer.js @@ -0,0 +1,224 @@ +"use strict"; + +const _normalizeSearchSymbol = (value) => + (value || "") + .toLowerCase() + .replace(/<|>|&|"|'|'| |…/g, " ") + .replace(/[^a-z0-9:]+/g, ""); + +const _splitSymbolWords = (value) => + (value || "") + .replace(/<|>|&|"|'|'| |…/g, " ") + .replace(/::/g, " ") + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .toLowerCase() + .match(/[a-z0-9]+/g) || []; + +const _getSearchQuery = () => { + try { + return new URLSearchParams(window.location.search).get("q") || ""; + } catch { + return ""; + } +}; + +var Scorer = { + // Keep strong object-name bias at the base layer. + objNameMatch: 80, + objPartialMatch: 35, + objPrio: { + 0: 25, // highest-priority API objects + 1: 10, + 2: -10, + }, + objPrioDefault: 0, + title: 15, + partialTitle: 7, + term: 5, + partialTerm: 2, + + score: (result) => { + const [docName, title, anchor, descr, baseScore, filename] = result; + let score = baseScore; + + const trimmedTitle = (title || "").trim(); + const trimmedAnchor = (anchor || "").trim(); + const trimmedDescription = (descr || "").trim(); + const trimmedFilename = (filename || "").trim(); + const trimmedDocName = (docName || "").trim(); + + const lowerDescription = trimmedDescription.toLowerCase(); + const lowerFilename = trimmedFilename.toLowerCase(); + const lowerDocName = trimmedDocName.toLowerCase(); + const query = _getSearchQuery().trim(); + const lowerQuery = query.toLowerCase(); + const normalizedQuery = _normalizeSearchSymbol(query); + + const titleParts = trimmedTitle.split("::").filter(Boolean); + const symbolDepth = titleParts.length; + const leaf = titleParts.length + ? titleParts[titleParts.length - 1] + : trimmedTitle; + const parentTitle = + titleParts.length > 1 ? titleParts.slice(0, -1).join("::") : ""; + const normalizedLeaf = _normalizeSearchSymbol(leaf); + const normalizedParent = _normalizeSearchSymbol(parentTitle); + const normalizedLeafOnlyQuery = _normalizeSearchSymbol( + query.includes("::") ? query.split("::").pop() : query, + ); + const leafWords = _splitSymbolWords(leaf); + const queryWords = _splitSymbolWords(query); + const simpleQuery = + lowerQuery && + !/[.:/_]/.test(lowerQuery) && + /^[a-z0-9]+$/.test(lowerQuery); + + const looksLikeNamespaceQualified = + /^[a-zA-Z_]\w*(::[a-zA-Z_]\w*)+/.test(trimmedTitle); + + const isExactFunctionishTitle = + /::[A-Za-z_]\w*$/.test(trimmedTitle); // e.g. thrust::transform + + const isClassishTitle = + /::[A-Z]\w*$/.test(trimmedTitle); // e.g. cub::DeviceRadixSort + + const isParameterLike = + /(template parameter|function parameter)/i.test(trimmedDescription); + const isMemberLike = + /(C\+\+ (member|type|property))/i.test(trimmedDescription); + const isCallableLike = + /(C\+\+ function\b|C\+\+ class\b|C\+\+ struct\b)/i.test(trimmedDescription); + const isTopLevelSymbol = looksLikeNamespaceQualified && symbolDepth <= 2; + const isNestedSymbol = symbolDepth >= 3; + const isConstructorLike = + isNestedSymbol && + _normalizeSearchSymbol(titleParts[symbolDepth - 2]) === normalizedLeaf; + const hasQueryInFilename = + lowerQuery && lowerFilename.includes(lowerQuery); + const hasQueryInDocName = lowerQuery && lowerDocName.includes(lowerQuery); + const isEnumeratorLike = + /(C\+\+ enumerator\b)/i.test(trimmedDescription) || /^[A-Z0-9_]+$/.test(leaf); + const isInternalHelperLike = + /(policy|dispatch|state|status|callback|preference|layout|runningprefixop|emptycallback|op)/i.test( + trimmedTitle, + ) || + /(TileState|Policy|Dispatch|Callback|Preference|Layout|RunningPrefixOp|Status|EmptyCallback)/.test( + trimmedTitle, + ); + const isPythonModuleLike = /(Python module\b)/i.test(trimmedDescription); + const leafStartsWithQueryWord = + queryWords.length === 1 && leafWords[0] === queryWords[0]; + const leafEndsWithQueryWord = + queryWords.length === 1 && + leafWords.length > 0 && + leafWords[leafWords.length - 1] === queryWords[0]; + const leafQueryRemainderWords = queryWords.length === 1 + ? leafWords.filter((word) => word !== queryWords[0]) + : []; + const hasCompactQueryWordRemainder = + leafStartsWithQueryWord && + leafQueryRemainderWords.length > 0 && + leafQueryRemainderWords.length <= 2; + const hasHelperSuffix = + /(Strategy|Policy|State|Status|Callback|Preference|Layout|Type|Op|Match|Functor|Tag|Traits|Descriptor|Counts)$/.test( + leaf, + ); + // Strong bias toward actual API symbols. + if (isExactFunctionishTitle) score += 35; + if (isClassishTitle) score += 20; + + // Small boost for anchored entries; these are often object targets. + if (trimmedAnchor) score += 5; + + // Penalize taxonomy/concept pages that match lots of body text. + if ( + lowerDescription.includes("thrust::") || + lowerDescription.includes("cub::") || + lowerDescription.includes("cuda::") + ) { + score += 8; + } + + // Query-aware ranking: prefer canonical symbol pages over nested members. + if (normalizedQuery) { + if (normalizedLeaf === normalizedLeafOnlyQuery) { + score += isTopLevelSymbol ? 180 : 35; + } else if ( + normalizedLeafOnlyQuery && + normalizedLeaf.includes(normalizedLeafOnlyQuery) + ) { + score += 15; + } + } + + if (isNestedSymbol) score -= 25; + if (isParameterLike) score -= 80; + if (isMemberLike) score -= 35; + if (isConstructorLike) score -= 30; + if (isCallableLike && isTopLevelSymbol) score += 20; + + // Prefer libcudacxx/cuda symbols over thrust equivalents on ties. + if ( + normalizedLeaf === normalizedLeafOnlyQuery && + /^cuda::/.test(trimmedTitle) + ) { + score += 12; + } + + // For plain keyword queries, prefer pages that match in title/path metadata. + if (simpleQuery) { + if (hasQueryInFilename || hasQueryInDocName) score += 45; + if (isInternalHelperLike) score -= 100; + if (isPythonModuleLike) score -= 30; + if (hasHelperSuffix) score -= 80; + if (isTopLevelSymbol && isCallableLike && !hasHelperSuffix) score += 40; + if ( + leafStartsWithQueryWord && + isTopLevelSymbol && + !isEnumeratorLike && + !isInternalHelperLike + ) { + score += 75; + } + if ( + leafEndsWithQueryWord && + isTopLevelSymbol && + !isEnumeratorLike && + !isInternalHelperLike + ) { + score += 45; + } + + // For broad prefix-style queries like "block", prefer compact public API + // names over longer compound variants or helper-like extensions. + if ( + hasCompactQueryWordRemainder && + isTopLevelSymbol && + !isEnumeratorLike && + !isInternalHelperLike && + !hasHelperSuffix + ) { + score += 70 - 15 * (leafQueryRemainderWords.length - 1); + } + // If a nested member matches the query but its parent symbol also does, + // prefer the parent page/class over the member overload. + if ( + isNestedSymbol && + normalizedLeaf === normalizedLeafOnlyQuery && + normalizedParent.includes(normalizedLeafOnlyQuery) + ) { + score -= 70; + } + if ( + isTopLevelSymbol && + normalizedLeaf.includes(normalizedLeafOnlyQuery) && + normalizedLeaf !== normalizedLeafOnlyQuery + ) { + score += 70; + } + } + + return score; + }, +}; diff --git a/cccl_upstream/docs/_templates/search.html b/cccl_upstream/docs/_templates/search.html new file mode 100644 index 00000000..9233b2c8 --- /dev/null +++ b/cccl_upstream/docs/_templates/search.html @@ -0,0 +1,12 @@ +{% extends "!nvidia_sphinx_theme/search.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block scripts %} + +{{ super() }} + +{% endblock %} diff --git a/cccl_upstream/docs/cccl/3.0_migration_guide.rst b/cccl_upstream/docs/cccl/3.0_migration_guide.rst new file mode 100644 index 00000000..e72e87b2 --- /dev/null +++ b/cccl_upstream/docs/cccl/3.0_migration_guide.rst @@ -0,0 +1,310 @@ +.. _cccl-3.0-migration-guide: + +CCCL 2.x ‐ CCCL 3.0 migration guide +=================================== + +The CCCL team plans breaking changes carefully and only conducts them at major releases. +The CCCL 2.8 release came with many deprecations to prepare for the breaking changes conducted in CCCL 3.0. +This page summarizes the changes and helps migrating from CCCL 2.x to CCCL 3.0. + +See also the `list of all deprecated APIs in CCCL 2.8 `_ +and the `list of breaking changes in CCCL 3.0 `_. + +CUDA Toolkit changes +-------------------- + +CCCL is moving to its own include directory within the CUDA Toolkit. This may cause build failures and some initial confusion. +This section will have some suggestions and mitigations to help maintain builds across both CUDA12 and future releases. + +The CTK-provided includes are changing in the following ways: + ++-------------------------------+------------------------------------+ +| **Before CUDA 13.0** | **After CUDA 13.0** | ++-------------------------------+------------------------------------+ +| `${CTK_ROOT}/include/cuda/` | `${CTK_ROOT}/include/cccl/cuda/` | ++-------------------------------+------------------------------------+ +| `${CTK_ROOT}/include/cub/` | `${CTK_ROOT}/include/cccl/cub/` | ++-------------------------------+------------------------------------+ +| `${CTK_ROOT}/include/thrust/` | `${CTK_ROOT}/include/cccl/thrust/` | ++-------------------------------+------------------------------------+ + +Due to these changes, and the fact that NVCC by default includes its own directories, you may encounter errors when including +CCCL headers in source files that are compiled *only* by the host compiler. + +For example, when compiling with GCC or MSVC alone, you may see ````, ````, or ```` headers missing. + +To mitigate this there are several solutions available depending on your build system: + +- **DO NOT** prefix missing includes with ```` -- This will break. +- CMake: link ``CCCL::CCCL`` to your target. + - Example: ``target_link_library(${MY_TARGET} PRIVATE CCCL::CCCL)`` +- Non-CMake: Directly include the CUDA Toolkit's CCCL directory. (Make/Other) + - Example: Add CCCL as an include flag ``-I${CTK_ROOT}/include/cccl`` +- Use a non-bundled CCCL. CCCL is available and maintained independently of the CTK. + - `See here for compatibility. `_ + +Removed macros +-------------- + +* ``CUB_IS_INT128_ENABLED``: No replacement +* ``CUB_MAX(a, b)``: Use the ``cuda::std::max(a, b)`` function instead +* ``CUB_MIN(a, b)``: Use the ``cuda::std::min(a, b)`` function instead +* ``CUB_QUOTIENT_CEILING(a, b)``: Use ``cuda::ceil_div(a, b)`` instead +* ``CUB_QUOTIENT_FLOOR(a, b)``: Use plain integer division ``a / b`` instead +* ``CUB_ROUND_DOWN_NEAREST(a, b)``: Use ``cuda::round_down(a, b)`` instead +* ``CUB_ROUND_UP_NEAREST(a, b)``: Use ``cuda::round_up(a, b)`` instead +* ``CUB_RUNTIME_ENABLED``: No replacement +* ``CUB_USE_COOPERATIVE_GROUPS``: No replacement +* ``CUDA_CUB_RET_IF_FAIL``: No replacement +* ``[THRUST|CUB]_CLANG_VERSION``: No replacement +* ``[THRUST|CUB]_DEVICE_COMPILER*``: No replacement +* ``[THRUST|CUB]_GCC_VERSION``: No replacement +* ``[THRUST|CUB]_HOST_COMPILER*``: No replacement +* ``[THRUST|CUB]_INCLUDE_DEVICE_CODE``: No replacement +* ``[THRUST|CUB]_INCLUDE_HOST_CODE``: No replacement +* ``[THRUST|CUB]_IS_DEVICE_CODE``: No replacement +* ``[THRUST|CUB]_IS_HOST_CODE``: No replacement +* ``[THRUST|CUB]_MSVC_VERSION_FULL``: No replacement +* ``[THRUST|CUB]_MSVC_VERSION``: No replacement +* ``THRUST_CDP_DISPATCH``: No replacement (Support for CUDA Dynamic Parallelism V1 (CDPv1) has been removed, see below) +* ``THRUST_DECLTYPE_RETURNS_WITH_SFINAE_CONDITION``: No replacement +* ``THRUST_DECLTYPE_RETURNS``: No replacement +* ``THRUST_DEVICE_CODE``: No replacement +* ``THRUST_HOST_BACKEND``: Use ``THRUST_HOST_SYSTEM`` instead +* ``THRUST_INLINE_CONSTANT``: Use ``inline constexpr`` instead +* ``THRUST_INLINE_INTEGRAL_MEMBER_CONSTANT``: Use ``static constexpr`` instead +* ``THRUST_LEGACY_GCC``: No replacement +* ``THRUST_MODERN_GCC_REQUIRED_NO_ERROR``: No replacement +* ``THRUST_MODERN_GCC``: No replacement +* ``THRUST_MVCAP``: No replacement +* ``THRUST_NODISCARD``: Use ``[[nodiscard]]`` instead +* ``THRUST_RETOF1``: No replacement +* ``THRUST_RETOF2``: No replacement +* ``THRUST_RETOF``: No replacement +* ``THRUST_TUNING_ARCH``: No direct replacement. Use compiler-specific ``__CUDA_ARCH__`` (nvcc) or ``__NVCOMPILER_CUDA_ARCH__`` (nvc++) instead + + +Removed functions and classes +----------------------------- + +* ``_ReadWriteBarrier`` and ``__thrust_compiler_fence``: Use ``cuda::atomic`` instead +* ``cub::*Kernel``: Any CUB kernel entrypoint is considered an implementation detail. No public exposure is provided. +* ``cub::Agent*``: CUB agents were considered implementation details and have all been moved to internal namespaces. No public exposure is provided. +* ``cub::AliasTemporaries``: No replacement +* ``cub::ArrayWrapper``: Use ``cuda::std::array`` instead +* ``cub::BAR``: No replacement +* ``cub::BaseTraits::CATEGORY``: Use the facilities from ```` instead +* ``cub::BaseTraits::NULL_TYPE``: No replacement +* ``cub::BaseTraits::PRIMITIVE``: Use the facilities from ```` instead +* ``cub::BFI``: Use ``cuda::bitfield_insert`` instead +* ``cub::BinaryOpHasIdxParam::HAS_PARAM``: Use ``cub::BinaryOpHasIdxParam::value`` instead +* ``cub::ConstantInputIterator``: Use ``thrust::constant_iterator`` instead +* ``cub::CountingInputIterator``: Use ``thrust::counting_iterator`` instead +* ``cub::CTA_SYNC_AND``: Use ``__syncthreads_and()`` instead +* ``cub::CTA_SYNC_OR``: Use ``__syncthreads_or()`` instead +* ``cub::CTA_SYNC``: Use ``__syncthreads()`` instead +* ``cub::Device*Policy``: Those policy hubs are considered implementation details. No public exposure is provided. +* ``cub::DeviceSpmv``: Use `cuSPARSE `_ instead +* ``cub::Difference``: Use ``cuda::std::minus`` instead +* ``cub::DivideAndRoundUp``: Use ``cuda::round_up`` instead +* ``cub::Division``: Use ``cuda::std::divides`` instead +* ``cub::Equality``: Use ``cuda::std::equal_to`` instead +* ``cub::FFMA_RZ``: No replacement +* ``cub::FMUL_RZ``: No replacement +* ``cub::FpLimits``: Use ``cuda::std::numeric_limits`` instead +* ``cub::GridBarrier``: Use the APIs from cooperative groups instead +* ``cub::GridBarrierLifetime``: Use the APIs from cooperative groups instead +* ``cub::IADD3``: No replacement +* ``cub::Inequality``: Use ``cuda::std::not_equal_to`` instead +* ``cub::Int2Type``: Use ``cuda::std::integral_constant`` instead +* ``cub::IterateThreadLoad``: No replacement +* ``cub::IterateThreadStore``: No replacement +* ``cub::KernelConfig``: No replacement +* ``cub::LaneId()``: Use ``cuda::ptx::get_sreg_laneid()`` instead +* ``cub::LaneMaskGe()``: Use ``cuda::ptx::get_sreg_lanemask_ge()`` instead +* ``cub::LaneMaskGt()``: Use ``cuda::ptx::get_sreg_lanemask_gt()`` instead +* ``cub::LaneMaskLe()``: Use ``cuda::ptx::get_sreg_lanemask_le()`` instead +* ``cub::LaneMaskLt()``: Use ``cuda::ptx::get_sreg_lanemask_lt()`` instead +* ``cub::MakePolicyWrapper``: No replacement +* ``cub::Max``: Use ``cuda::maximum`` instead +* ``cub::max``: Use ``cuda::std::max`` instead +* ``cub::MemBoundScaling``: No replacement +* ``cub::Min``: Use ``cuda::minimum`` instead +* ``cub::min``: Use ``cuda::std::min`` instead +* ``cub::Mutex``: Use ``std::mutex`` instead +* ``cub::PolicyWrapper``: No replacement +* ``cub::PRMT``: Use ``cuda::ptx::prmt()`` instead +* ``cub::RegBoundScaling``: No replacement +* ``cub::SHFL_IDX_SYNC``: Use ``__shfl_sync()`` instead +* ``cub::SHL_ADD``: No replacement +* ``cub::SHR_ADD``: No replacement +* ``cub::Sum``: Use ``cuda::std::plus`` instead +* ``cub::Swap(a, b)``: Use ``cuda::std::swap(a, b)`` instead +* ``cub::ThreadTrap()``: Use ``cuda::std::terminate()`` instead +* ``cub::TransformInputIterator``: Use ``thrust::transform_iterator`` instead +* ``cub::TripleChevronFactory``: No replacement for now, we are working on a new kernel launch facility +* ``cub::ValueCache``: No replacement +* ``cub::WARP_ALL``: Use ``__all_sync()`` instead +* ``cub::WARP_ANY``: Use ``__any_sync()`` instead +* ``cub::WARP_BALLOT``: Use ``__ballot_sync()`` instead +* ``cub::WARP_SYNC``: Use ``__syncwarp()`` instead +* ``cub::WarpId()``: Use ``cuda::ptx::get_sreg_warpid()`` instead +* ``thrust::*::[first_argument_type|second_argument_type|result_type]``: The nested aliases have been removed for all function object types: ``thrust::[plus|minus|multiplies|divides|modulus|negate|square|equal_to|not_equal_to|greater|less|greater_equal|less_equal|logical_and|logical_or|logical_not|bit_and|bit_or|bit_xor|identity|maximum|minimum|project1st|project2nd]``. No replacement. +* ``thrust::[unary|binary]_function``: No replacement. If you inherit from one of these types, just remove those base classes. +* ``thrust::[unary|binary]_traits``: No replacement. +* ``thrust::async::*``: No replacement for now. We are working on a C++26 senders implementation. For make a thrust algorithm skip syncing, use ``thrust::cuda::par_nosync`` as execution policy. +* ``thrust::bidirectional_universal_iterator_tag``: No replacement +* ``thrust::conjunction_value``: Use ``cuda::std::bool_constant<(Ts && ...)>`` instead +* ``thrust::conjunction_value_v``: Use a fold expression: ``Ts && ...`` instead +* ``thrust::cuda_cub::core::*``: Those are considered implementation details. No public exposure is provided. +* ``thrust::cuda_cub::counting_iterator_t``: Use ``thrust::counting_iterator`` instead +* ``thrust::cuda_cub::identity``: Use ``cuda::std::identity`` instead +* ``thrust::cuda_cub::launcher::triple_chevron``: No replacement for now, we are working on a new kernel launch facility +* ``thrust::cuda_cub::terminate``: Use ``cuda::std::terminate()`` instead +* ``thrust::cuda_cub::transform_input_iterator_t``: Use ``thrust::transform_iterator`` instead +* ``thrust::cuda_cub::transform_pair_of_input_iterators_t``: Use ``thrust::transform_iterator of a thrust::zip_iterator`` instead +* ``thrust::disjunction_value``: Use ``cuda::std::bool_constant<(Ts || ...)>`` instead +* ``thrust::disjunction_value_v``: Use a fold expression: ``Ts || ...`` instead +* ``thrust::forward_universal_iterator_tag``: No replacement +* ``thrust::identity``: Use ``cuda::std::identity`` instead. If ``thrust::identity`` was used to perform a cast to ``T``, please define your own function object. +* ``thrust::input_universal_iterator_tag``: No replacement +* ``thrust::negation_value``: Use ``cuda::std::bool_constant`` instead +* ``thrust::negation_value_v``: Use a plain negation ``!T`` +* ``thrust::not[1|2]``: Use ``cuda::std::not_fn`` instead +* ``thrust::null_type``: No replacement +* ``thrust::numeric_limits``: Use ``cuda::std::numeric_limits`` instead +* ``thrust::optional``: Use ``cuda::std::optional`` instead. +* ``thrust::output_universal_iterator_tag``: No replacement +* ``thrust::random_access_universal_iterator_tag``: No replacement +* ``thrust::remove_cvref[_t]``: Use ``cuda::std::remove_cvref[_t]`` instead +* ``thrust::void_t``: Use ``cuda::std::void_t`` instead + + +Deprecations with planned removal +--------------------------------- + +* ``CUB_LOG_SMEM_BANKS``: No replacement +* ``CUB_LOG_WARP_THREADS``: No replacement +* ``CUB_MAX_DEVICES``: No replacement +* ``CUB_PREFER_CONFLICT_OVER_PADDING``: No replacement +* ``CUB_PTX_LOG_SMEM_BANKS``: No replacement +* ``CUB_PTX_LOG_WARP_THREADS``: No replacement +* ``CUB_PTX_PREFER_CONFLICT_OVER_PADDING``: No replacement +* ``CUB_PTX_SMEM_BANKS``: No replacement +* ``CUB_PTX_SUBSCRIPTION_FACTOR``: No replacement +* ``CUB_PTX_WARP_THREADS``: No replacement +* ``CUB_SMEM_BANKS``: No replacement +* ``CUB_SUBSCRIPTION_FACTOR``: No replacement +* ``CUB_WARP_THREADS``: No replacement +* ``THRUST_FALSE``: No replacement +* ``THRUST_PREVENT_MACRO_SUBSTITUTION``: No replacement +* ``THRUST_STATIC_ASSERT(expr)``: Use ``static_assert(expr)`` instead +* ``THRUST_TRUE``: No replacement +* ``THRUST_UNKNOWN``: No replacement +* ``THRUST_UNUSED_VAR``: No replacement +* ``cub::BFE``: Use ``cuda::bitfield_extract`` instead +* ``cub::MergePathSearch``: No replacement +* ``cub::Traits::Max()``: Use ``cuda::std::numeric_limits::max()`` instead +* ``cub::Traits::Min()``: Use ``cuda::std::numeric_limits::min()`` instead +* ``thrust::iterator_difference[_t]``: Use ``cuda::std::iterator_traits::difference_type`` or ``cuda::std::iter_difference_t`` instead +* ``thrust::iterator_pointer[_t]``: Use ``cuda::std::iterator_traits::pointer`` instead +* ``thrust::iterator_reference[_t]``: Use ``cuda::std::iterator_traits::reference`` or ``cuda::std::iter_reference_t`` instead +* ``thrust::iterator_traits``: Use ``cuda::std::iterator_traits`` instead +* ``thrust::iterator_value[_t]``: Use ``cuda::std::iterator_traits::value_type`` or ``cuda::std::iter_value_t`` instead + + +API breaks +---------- + +* ``cub::Block*``: All trailing ``int LEGACY_PTX_ARCH`` template parameters have been removed +* ``cub::CachingAllocator``: The constructor taking a trailing ``bool debug`` parameter has been removed +* ``cub::Device*``: All overloads with a trailing ``bool debug_synchronous`` parameter have been removed +* ``cub::Dispatch*``: All Boolean template parameters have been replaced by enumerations to increase readability +* ``cub::Dispatch*``: All policy hub template parameters have been moved to the back of the template parameters list +* ``cub::DispatchScan[ByKey]``: The offset type must be an unsigned type of at least 4-byte size +* ``cuda::ceil_div``: Now returns the common type of its arguments +* ``thrust::pair``: Is now an alias to ``cuda::std::pair`` and no longer a distinct type +* ``thrust::tabulate_output_iterator``: The ``value_type`` has been fixed to be ``void`` +* ``thrust::transform_iterator``: Upon copying, will now always copy its contained function. If the contained function is neither copy constructible nor copy assignable, the iterator fails to compile when attempting to be copied. +* ``thrust::tuple``: Is now an alias to ``cuda::std::tuple`` and no longer a distinct type +* ``thrust::universal_host_pinned_memory_resource``: The alias has changed to a different memory resource, potentially changing pointer types derived from an allocator/container using this memory resource. +* The following Thrust function object types have been made aliases to the equally-named types in ``cuda::std::``: ``thrust::[plus|minus|multiplies|divides|modulus|negate|equal_to|not_equal_to|greater|less|greater_equal|less_equal|logical_and|logical_or|logical_not|bit_and|bit_or|bit_xor|identity|maximum|minimum]``. No replacement. +* ``CUB_DEFINE_DETECT_NESTED_TYPE``: The generated detector trait no longer provides a ``::VALUE`` member. Use ``::value`` instead. + + +Iterator traits +^^^^^^^^^^^^^^^ + +``cuda::std::iterator_traits`` will now correctly recognize user-provided specializations of ``std::iterator_traits``. +All of Thrust's iterator traits have been redefined in terms of ``cuda::std::iterator_traits``, +and users should prefer to use iterator traits from libcu++. +``thrust::iterator_traits`` can no longer be specialized. +Users should prefer to specialize ``cuda::std::iterator_traits`` instead of ``std::iterator_traits`` when necessary, +to make their iterators work equally in device code. + + +CUB Traits +^^^^^^^^^^ + +The functionality and internal use of ``cub::Traits`` has been minimized, because libcu++ provides better and standard alternatives. +Only the use in CUB's radix sort implementation for bit-twiddling remains. +Floating-point limits should be obtained using ``cuda::std::numeric_limits`` instead of ``cub::FpLimits``. +Classification of types should be done with the facilities from ```` and ````, +notably with ``cuda::std::is_signed[_v]``, ``cuda::std::is_integral[_v]``, etc. +There is an important difference for extended floating point types though: +Since ``cuda::std::is_floating_point[_v]`` will only recognize C++ standard floating point types, +``cuda::is_floating_point[_v]`` must be used to correctly classify extended floating point types like ``__half`` or ``__nv_bfloat16``. +``cub::BaseTraits`` and ``cub::Traits`` can no longer be specialized for custom types, and ``cub::FpLimits`` has been removed. + +We acknowledge the need to provide user-defined floating point types though, +e.g., registering a custom half type with CUB to be used in radix sort. +Therefore, users can still specialize ``cub::NumericTraits`` for their custom floating point types, +inheriting from ``cub::BaseTraits`` and providing the necessary information for the type. +Additionally, the traits from libcu++ have to be specialized as well: + +For example, a custom floating point type ``my_half`` could be registered with CUB and libcu++ like this: + +.. code:: cpp + + template <> + inline constexpr bool ::cuda::is_floating_point_v = true; + + template <> + class ::cuda::std::numeric_limits { + public: + static constexpr bool is_specialized = true; + static __host__ __device__ my_half max() { return /* TODO */; } + static __host__ __device__ my_half min() { return /* TODO */; } + static __host__ __device__ my_half lowest() { return /* TODO */; } + }; + + template <> + struct CUB_NS_QUALIFIER::NumericTraits : BaseTraits {}; + + +Behavioral changes +------------------ + +* ``cub::DeviceReduce::[Arg][Max|Min]``: Will now use ``cuda::std::numeric_limits::[max|min]()`` instead of ``cub::Traits`` to determine the initial value +* ``cuda::std::mdspan``: The implementation was entirely rewritten and you may experience subtle behavioral changes +* ``thrust::transform_iterator``: The logic to determine the reference type has been reworked, especially wrt. to functions that return references to their own arguments (e.g., ``thrust::identity``). +* ``thrust::transform_iterator::difference_type``: The logic to select the difference type has been reworked. It's now either ``int`` or ``ptrdiff``. + + +ABI breaks +---------- + +* All of libcu++'s old ABI namespaces have been removed + + +Platform support +---------------- + +* At least C++17 is required +* At least clang 14 is required +* At least GCC 7 is required +* On Windows, at least Visual Studio 2019 is required (MSC_VER >= 1920) +* Intel ICC (``icpx``) is no longer supported +* At least CUDA Toolkit 12.0 is required +* Support for CUDA Dynamic Parallelism V1 (CDPv1) has been removed +* At least a GPU with compute capability 50 (Maxwell) is required diff --git a/cccl_upstream/docs/cccl/config_macros.rst b/cccl_upstream/docs/cccl/config_macros.rst new file mode 100644 index 00000000..78f73598 --- /dev/null +++ b/cccl_upstream/docs/cccl/config_macros.rst @@ -0,0 +1,94 @@ +.. _cccl-config: + +CCCL configuration macros +========================= + +The CUDA Core Compute Libraries provide a set of macros to enable or disable specific features. These macros must be defined before any CCCL source file is included. The recommended way is to define them as the predefined compiler macros, for example: + +.. code-block:: bash + + nvcc -DCCCL_DISABLE_SOME_FEATURE src.cu + +.. important:: + These macros should be defined consistently in the whole project. Defining them only for some translation units may lead to unexpected compile time and runtime behaviour. + +Assertion Control Macros +------------------------ ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_ENABLE_ASSERTIONS | Enables assertions in both host and device code. Implied by compiling in debug mode. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_ENABLE_DEVICE_ASSERTIONS | Enables assertions in device code, independent of debug mode. Implied by ``CCCL_ENABLE_ASSERTIONS``. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_ENABLE_HOST_ASSERTIONS | Enables assertions in host code, independent of debug mode. Implied by ``CCCL_ENABLE_ASSERTIONS``. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +C++ Feature Control Macros +-------------------------- ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_EXCEPTIONS | Disables throwing exceptions. Each ``throw`` is replaced by a call to ``cuda::std::terminate()``. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_RTTI | Disables use of runtime type information. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_IGNORE_MSVC_TRADITIONAL_PREPROCESSOR_WARNING | Disables diagnostics emitted when using MSVC's traditional preprocessor. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +CUDA Feature Control Macros +--------------------------- ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_ARCH_DEPENDENT_NAMESPACE | Disables architecture dependent name mangling of kernels. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_CDP | Disables use of CUDA Dynamic Parallelism. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_CTK_COMPATIBILITY_CHECK | Disables the check whether NVCC's version matches the CUDA Toolkit version. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_DEVICE_RUNTIME | Disables use of CUDA device runtime APIs (````), thus makes some APIs that are ``__host__ __device__`` to be ``__host__`` only. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_LAUNCH_BOUNDS | Disables use of ``__launch_bounds__`` attribute. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_PDL | Disables use of Programmatic Dependent Launch. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Deprecation Diagnostics Suppression Macros +------------------------------------------ ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_IGNORE_DEPRECATED_API | Disables deprecated API diagnostics. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_IGNORE_DEPRECATED_COMPILER | Disables deprecated compiler diagnostics. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_IGNORE_DEPRECATED_CPP_DIALECT | Disables deprecated C++ dialect diagnostics. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_IGNORE_DEPRECATED_CUDA_BELOW_12 | Disables deprecated CUDA compiler diagnostics. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Third Party Libraries Interoperability +-------------------------------------- ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_DLPACK | Disables inclusion of DLPack header and APIs. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Type Support +------------ ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_BF16_SUPPORT | Disables use and library support for the ``__nv_bfloat16`` type. Also disables support for smaller NV floating point types. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_FLOAT128_SUPPORT | Disables use and library support for the ``__float128`` type. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_FP16_SUPPORT | Disables use and library support for the ``__half`` type. Also disables support for smaller NV floating point types. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_INT128_SUPPORT | Disables use and library support for the ``__int128`` type. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_LONG_DOUBLE_SUPPORT | Disables use and library support for the ``long double`` type. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_NVFP4_SUPPORT | Disables use and library support for the ``__nv_fp4_eNmM`` types. Also disables support for smaller NV floating point types. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_NVFP6_SUPPORT | Disables use and library support for the ``__nv_fp6_eNmM`` types. Also disables support for smaller NV floating point types. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_DISABLE_NVFP8_SUPPORT | Disables use and library support for the ``__nv_fp8_eNmM`` types. Also disables support for smaller NV floating point types. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_ENABLE_EXPERIMENTAL_HOST_ATOMICS_128B | Enables experimental support for 128b atomics in host code. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ +| CCCL_GCC_HAS_EXTENDED_NUMERIC_LITERALS | Must be enabled in addition to passing ``-fext-numeric-literals`` with GCC to enable ``__float128`` support. | ++---------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + +.. note:: + ```` headers contain dependencies on other ```` headers, thus for example defining ``CCCL_DISABLE_BF16_SUPPORT`` will disable support for NVIDIA 8-bit, 6-bit and 4-bit floating point types, too. diff --git a/cccl_upstream/docs/cccl/contributing.rst b/cccl_upstream/docs/cccl/contributing.rst new file mode 100644 index 00000000..948e152f --- /dev/null +++ b/cccl_upstream/docs/cccl/contributing.rst @@ -0,0 +1,14 @@ +.. _cccl-contributing: + +Contributing to the CUDA Core Compute Libraries +=============================================== + +.. toctree:: + :maxdepth: 1 + + contributing/code_of_conduct + +We welcome contributions - just send us a pull request! +You can find detailed instructions on `GitHub `_. + +libcu++ uses the `Apache License v2.0 with LLVM Exceptions `_. diff --git a/cccl_upstream/docs/cccl/contributing/code_of_conduct.rst b/cccl_upstream/docs/cccl/contributing/code_of_conduct.rst new file mode 100644 index 00000000..5250320c --- /dev/null +++ b/cccl_upstream/docs/cccl/contributing/code_of_conduct.rst @@ -0,0 +1,105 @@ +Code of Conduct +=============== + +Overview +-------- + +This document defines the Code of Conduct followed and enforced for +NVIDIA C++ Core Compute Libraries. + +Intended Audience +~~~~~~~~~~~~~~~~~ + +- Community +- Developers +- Project Leads + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +Our Standards +------------- + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language. +- Being respectful of differing viewpoints and experiences. +- Gracefully accepting constructive criticism. +- Focusing on what is best for the community. +- Showing empathy towards other community members. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual + attention or advances. +- Trolling, insulting/derogatory comments, and personal or political + attacks. +- Public or private harassment. +- Publishing others' private information, such as a physical or + electronic address, without explicit permission. +- Other conduct which could reasonably be considered inappropriate. + +Our Responsibilities +-------------------- + +Project maintainers are responsible for clarifying the standards of +acceptable behavior and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, or to ban +temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +Scope +----- + +This Code of Conduct applies both within project spaces and in public +spaces when an individual is representing the project or its community. +Examples of representing a project or community include using an +official project email address, posting via an official social media +account, or acting as an appointed representative at an online or +offline event. Representation of a project may be further defined and +clarified by project maintainers. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported by contacting cpp-conduct@nvidia.com. All complaints will be +reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an +incident. Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in +good faith may face temporary or permanent repercussions as determined +by other members of the project's leadership. + +Attribution +----------- + +This Code of Conduct was taken from the `NVIDIA +RAPIDS `_ project, which was +adapted from the `Contributor Covenant version +1.4 `_. + +Please see this `FAQ `_ for +answers to common questions about this Code of Conduct. + +Contact +------- + +Please email cpp-conduct@nvidia.com for any Code of Conduct related +matters. diff --git a/cccl_upstream/docs/cccl/determinism.rst b/cccl_upstream/docs/cccl/determinism.rst new file mode 100644 index 00000000..3e26c604 --- /dev/null +++ b/cccl_upstream/docs/cccl/determinism.rst @@ -0,0 +1,106 @@ +.. _cccl-determinism: + +Determinism +=========== + +Determinism describes whether an algorithm produces the *same result* every time it is run with the +same input. For many parallel algorithms this is not automatic. For reductions and scans, for example, +the order in which partial results are combined depends on how work is scheduled across thousands of +threads, and that schedule can change between launches or between GPUs. When the combining operator is +not perfectly associative — most notably floating-point addition, where ``(a + b) + c`` need not equal +``a + (b + c)`` — a different combining order yields a (slightly) different result, so the output is no +longer identical from one run to the next. + +What counts as the "same result" is defined *per algorithm*. For reductions and scans it means a +*bitwise-identical* output. For other algorithms it can be weaker: a deterministic top-k, for example, +guarantees the same *set* of selected items, while the order of those items within the output is a +separate guarantee that an algorithm may expose on its own. + +CCCL lets users state the determinism guarantee they need as an explicit *requirement* on an +algorithm, rather than relying on implementation-defined behavior. The library then either +selects an implementation that satisfies the requirement or rejects the call at compile time if the +requirement cannot be met for the given types and operator. + +Determinism guarantees +---------------------- + +By *reproducible* we mean: given the same inputs, an algorithm returns the same output, in the sense +defined for that algorithm (see above). What the guarantees below differ in is the *scope* of that +reproducibility — across repeated runs, across hardware, or not at all. CCCL models three levels, +defined in ``cuda::execution::determinism``: + +``not_guaranteed`` + No reproducibility guarantee. The result is a valid answer, but it may differ from one invocation to + the next — even on the same GPU with the same input. This is usually the fastest option. + +``run_to_run`` + The result is reproducible across repeated runs *on the same GPU*, with the same input, build, + tuning, and launch configuration. It may still differ on a *different* GPU architecture. + +``gpu_to_gpu`` + The strongest guarantee: the result is reproducible across repeated runs *and across different GPU + architectures* — the same inputs yield the same bits whether the algorithm runs on, say, an Ampere or + a Hopper GPU. This is the most constrained option, is not available for every type/operator + combination, and is typically the slowest. + +The guarantees are ordered from weakest to strongest: +``not_guaranteed`` ⊆ ``run_to_run`` ⊆ ``gpu_to_gpu``. A ``gpu_to_gpu`` result is also reproducible +run-to-run, and a ``run_to_run`` result is a valid (but stronger-than-required) answer wherever +``not_guaranteed`` would be accepted. + +For types and operators that are exactly associative (see +:ref:`cuda::is_associative_v `; for example, integral +addition with well-known operators), every invocation is already reproducible across runs and GPUs, so the +stronger guarantees come for free and the library simply selects the fastest valid implementation. + +.. warning:: + + ``gpu_to_gpu``/``run_to_run`` reproducibility is guaranteed for a *fixed* CCCL and CUDA Toolkit version, not + across versions. If a policy selector is specified to change the used tuning, then reproducibility is only + guaranteed for identical tunings. The bitwise result may also change between CCCL or CUDA Toolkit releases as + algorithms, reduction structures, or tuning evolve. + +Requesting a determinism guarantee +----------------------------------- + +Determinism is expressed as a *requirement* and passed to an algorithm through its execution +environment using ``cuda::execution::require``: + +.. code-block:: c++ + + #include + + // Request run-to-run reproducibility for this call. + auto env = cuda::execution::require(cuda::execution::determinism::run_to_run); + +The requirement may be combined with other environment properties — such as a stream or a memory +resource — into a single environment: + +.. code-block:: c++ + + auto determinism = cuda::execution::require(cuda::execution::determinism::run_to_run); + auto env = cuda::std::execution::env{cuda::stream_ref{stream}, memory_resource, determinism}; + +Passing a determinism property *without* wrapping it in ``require`` is a compile-time error +(*"Determinism should be used inside requires to have an effect."*). ``require`` turns the property +into a *requirement*, which is what the algorithm honors — this prevents a stray determinism property +from being silently ignored. + +If an algorithm cannot satisfy the requested guarantee for the given value type and operator, the call +fails to compile with a diagnostic explaining the constraint. If the guarantee can be satisfied by a +weaker-but-sufficient implementation (for example, an exactly-associative operator under +``gpu_to_gpu``), the library transparently selects it. + +Where it is used +---------------- + +Determinism requirements are consumed today by several ``cub`` device algorithms. See the +:ref:`CUB determinism guide ` for the per-algorithm support matrix, the exact +type/operator constraints, and some examples. + +Further reading +--------------- + +- `Controlling Floating-Point Determinism in NVIDIA CCCL + `_ — a + deeper walkthrough of the three guarantees and the implementation strategies behind them. diff --git a/cccl_upstream/docs/cccl/development/build_and_bisect_tools.rst b/cccl_upstream/docs/cccl/development/build_and_bisect_tools.rst new file mode 100644 index 00000000..f47d2bea --- /dev/null +++ b/cccl_upstream/docs/cccl/development/build_and_bisect_tools.rst @@ -0,0 +1,136 @@ +.. _build-and-bisect-tools: + +Build and Bisect Utilities +========================== + +``build_and_test_targets.sh`` +----------------------------- + +:file:`ci/util/build_and_test_targets.sh` configures, builds, and tests selected +CMake targets. + +Options +~~~~~~~ +- ``--preset `` - choose a CMake preset. +- ``--cmake-options `` - extra arguments for the preset configuration. +- ``--configure-override `` - run a custom configuration command instead of + a preset. When used, ``--preset`` and ``--cmake-options`` are ignored. +- ``--build-targets `` - space separated Ninja targets. If omitted, + nothing builds. +- ``--ctest-targets `` - space separated CTest ``-R`` patterns. If + omitted, nothing runs. +- ``--lit-precompile-tests `` - space separated libcudacxx lit test paths + to precompile (no run). Paths are relative to ``libcudacxx/test/libcudacxx/``. +- ``--lit-tests `` - space separated libcudacxx lit test paths to execute. + Paths are relative to ``libcudacxx/test/libcudacxx/``. +- ``--custom-test-cmd `` - arbitrary command executed after build/tests. + +Combine with ``.devcontainer/launch.sh -d`` to reproduce CI commands inside a +container and choose a CUDA toolkit and host compiler: +``.devcontainer/launch.sh -d [--cuda ] [--host ] [--gpus all] --