diff --git a/.github/actions/setup-hardened-python/action.yml b/.github/actions/setup-hardened-python/action.yml index 8afb6feef2b..b5b909c9043 100644 --- a/.github/actions/setup-hardened-python/action.yml +++ b/.github/actions/setup-hardened-python/action.yml @@ -26,6 +26,11 @@ inputs: venv: description: Directory for the venv. Defaults to a new directory under RUNNER_TEMP. default: "" + stack-reserve: + description: >- + Windows only. Raise the base interpreter's main-thread stack reserve to at + least this many bytes. Empty (default) leaves the interpreter unchanged. + default: "" outputs: python-path: @@ -123,3 +128,41 @@ runs: PYTHON: ${{ steps.venv.outputs.python-path }} run: | "${PYTHON}" -m pip install -U pip + + - name: Raise the interpreter's stack reserve + if: runner.os == 'Windows' && inputs.stack-reserve != '' + shell: bash --noprofile --norc -xeuo pipefail {0} + env: + PYTHON: ${{ steps.venv.outputs.python-path }} + STACK_RESERVE: ${{ inputs.stack-reserve }} + run: | + # sys.stdout.write avoids a trailing CR from Windows Python's CRLF. + base="$("${PYTHON}" -c 'import sys; sys.stdout.write(sys._base_executable)')" + # Windows cannot overwrite a running executable. Prepare a copy and + # replace the original after the patching interpreter exits. + uv run --no-project --isolated --with pefile==2024.8.26 --python "${base}" python - "${base}" "${STACK_RESERVE}" <<'EOF' + import sys + + import pefile + + path, reserve = sys.argv[1], int(sys.argv[2]) + if reserve <= 0: + raise ValueError("stack-reserve must be a positive number of bytes") + with pefile.PE(path) as pe: + pe.OPTIONAL_HEADER.SizeOfStackReserve = max(pe.OPTIONAL_HEADER.SizeOfStackReserve, reserve) + pe.write(path + ".stack-reserve") + EOF + mv -f -- "${base}.stack-reserve" "${base}" + # Verify the running interpreter's stack, rather than just its header. + "${base}" - "${STACK_RESERVE}" <<'EOF' + import ctypes + import sys + + lo, hi = ctypes.c_size_t(), ctypes.c_size_t() + get_limits = ctypes.windll.kernel32.GetCurrentThreadStackLimits + get_limits.argtypes = [ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(ctypes.c_size_t)] + get_limits.restype = None + get_limits(ctypes.byref(lo), ctypes.byref(hi)) + print("main-thread stack reserve:", hi.value - lo.value) + sys.exit(hi.value - lo.value < int(sys.argv[1])) + EOF diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2e998453e5c..f75ac200b06 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -242,22 +242,15 @@ jobs: submodules: true - name: Set up Python - uses: ./.github/actions/setup-hardened-python + uses: $/.github/actions/setup-hardened-python with: python-version: ${{ env.PY_VER }} venv: .venv + stack-reserve: 8388608 - name: Set up MSVC uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 - - name: Raise the interpreter's stack reserve to 8 MiB - # Importing the coverage-built cuda.bindings.driver overflows the - # 2,000,000-byte main-thread stack of python-build-standalone (#3330). - run: | - base_py="$(.venv/Scripts/python -c 'import sys; print(sys._base_executable)')" - editbin -nologo -stack:8388608 "$base_py" - dumpbin -nologo -headers "$base_py" | grep -i "size of stack reserve" - - name: Set up mini CTK uses: ./.github/actions/fetch_ctk continue-on-error: false @@ -381,10 +374,11 @@ jobs: nvidia-smi - name: Set up Python - uses: ./.github/actions/setup-hardened-python + uses: $/.github/actions/setup-hardened-python with: python-version: ${{ env.PY_VER }} venv: .venv + stack-reserve: 8388608 - name: Set up mini CTK uses: ./.github/actions/fetch_ctk @@ -435,22 +429,22 @@ jobs: cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" "$GITHUB_WORKSPACE/.venv/Scripts/pytest" -v --cov=./cuda --cov-append --cov-context=test --cov-config="$GITHUB_WORKSPACE/.coveragerc" "$GITHUB_WORKSPACE/cuda_pathfinder/tests" - # Cython linetrace under coverage on Windows needs more stack than the - # default 1 MB thread size. The helper runs pytest on an 8 MB thread. - - name: Run cuda.bindings tests (with 8MB stack) + # The patched interpreter provides the larger stack on the main thread + # and in child interpreters. Deferred cleanup needs pytest on the main thread. + - name: Run cuda.bindings tests continue-on-error: true run: | - "$GITHUB_WORKSPACE/.venv/Scripts/python" "$GITHUB_WORKSPACE/ci/tools/run_pytest_with_stack.py" \ - --cwd "${{ steps.install-root.outputs.INSTALL_ROOT }}" \ + cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" + "$GITHUB_WORKSPACE/.venv/Scripts/python" -m pytest \ -v --cov=./cuda --cov-append --cov-context=test \ --cov-config="$GITHUB_WORKSPACE/.coveragerc" \ "$GITHUB_WORKSPACE/cuda_bindings_13/tests" - - name: Run cuda.core tests (with 8MB stack) + - name: Run cuda.core tests continue-on-error: true run: | - "$GITHUB_WORKSPACE/.venv/Scripts/python" "$GITHUB_WORKSPACE/ci/tools/run_pytest_with_stack.py" \ - --cwd "${{ steps.install-root.outputs.INSTALL_ROOT }}" \ + cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" + "$GITHUB_WORKSPACE/.venv/Scripts/python" -m pytest \ -v --cov=./cuda --cov-append --cov-context=test \ --cov-config="$GITHUB_WORKSPACE/.coveragerc" \ "$GITHUB_WORKSPACE/cuda_core/tests" diff --git a/ci/tools/run_pytest_with_stack.py b/ci/tools/run_pytest_with_stack.py deleted file mode 100644 index e1e1f782ba1..00000000000 --- a/ci/tools/run_pytest_with_stack.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Run pytest on a thread with a larger stack size. - -Cython linetrace instrumentation under coverage on Windows can exceed the -default 1 MB thread stack. This helper spawns a single worker thread with -a configurable stack (default 8 MB) so the rest of the CI workflow stays -readable. - -Usage: - python run_pytest_with_stack.py [--stack-mb N] [--cwd DIR] [pytest args ...] -""" - -import argparse -import concurrent.futures -import os -import sys -import threading - -import pytest - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--stack-mb", - type=int, - default=8, - help="Thread stack size in megabytes (default: 8)", - ) - parser.add_argument( - "--cwd", - default=None, - help="Working directory for the test run", - ) - args, pytest_args = parser.parse_known_args() - - if args.cwd: - os.chdir(args.cwd) - - threading.stack_size(args.stack_mb * 1024 * 1024) - - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - code = pool.submit(pytest.main, pytest_args).result() - - sys.exit(code) - - -if __name__ == "__main__": - main()