From 7af1fc7600d8b8838012b551b48bf9e29d170c24 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 03:57:16 -0700 Subject: [PATCH 01/10] Enable free-threaded CPython support --- mkl_random/mklrand.pyx | 1 + pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mkl_random/mklrand.pyx b/mkl_random/mklrand.pyx index edbcd400..215ae25d 100644 --- a/mkl_random/mklrand.pyx +++ b/mkl_random/mklrand.pyx @@ -24,6 +24,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # cython: language_level=3 +# cython: freethreading_compatible=True cdef extern from "Python.h": void* PyMem_Malloc(size_t n) diff --git a/pyproject.toml b/pyproject.toml index c841c9a3..983c3601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ build-backend = "mesonpy" requires = [ "meson-python>=0.16.0", "ninja", - "Cython", + "Cython>=3.1.0", "numpy", "mkl-devel", "cmake" @@ -50,6 +50,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Free Threading :: 2 - Beta", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development", "Topic :: Scientific/Engineering", From ac81826eebc16006d03a7f64f8c0fa858b90d8dc Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 03:58:02 -0700 Subject: [PATCH 02/10] Update conda recipes for free-threaded builds --- conda-recipe-cf/meta.yaml | 4 +--- conda-recipe/meta.yaml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/conda-recipe-cf/meta.yaml b/conda-recipe-cf/meta.yaml index c1dcd498..96b019f5 100644 --- a/conda-recipe-cf/meta.yaml +++ b/conda-recipe-cf/meta.yaml @@ -21,15 +21,13 @@ requirements: - cmake - ninja - python - - python-gil # [py>=314] - mkl-devel - - cython + - cython >=3.1.0 - numpy - pip - wheel >=0.41.3 run: - python - - python-gil # [py>=314] - {{ pin_compatible('numpy', min_pin="x.x", max_pin="x") }} - {{ pin_compatible('mkl', min_pin="x.x", max_pin="x") }} diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index dbdedbbe..03d03ca0 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -24,15 +24,13 @@ requirements: - cmake - ninja - python - - python-gil # [py>=314] - mkl-devel - - cython + - cython >=3.1.0 - numpy - pip - wheel >=0.41.3 run: - python - - python-gil # [py>=314] - {{ pin_compatible('mkl', min_pin="x.x", max_pin="x") }} - numpy >=1.26.4 From dd3c9c99756bfb25b435cc1a043ee0ff5e3a8ee1 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 04:13:05 -0700 Subject: [PATCH 03/10] Extend GH workflow matrices with free-threaded Python 3.14t --- .github/workflows/build-with-clang.yml | 4 +- .../workflows/build-with-standard-clang.yml | 4 +- .github/workflows/build_pip.yml | 19 +++- .github/workflows/conda-package-cf.yml | 90 +++++++++++++------ .github/workflows/conda-package.yml | 74 ++++++++++----- 5 files changed, 137 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build-with-clang.yml b/.github/workflows/build-with-clang.yml index 45137e15..c3a58aca 100644 --- a/.github/workflows/build-with-clang.yml +++ b/.github/workflows/build-with-clang.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] numpy_version: ["'numpy>=2'"] env: ONEAPI_ROOT: /opt/intel/oneapi @@ -53,7 +53,7 @@ jobs: - name: Install mkl_random dependencies run: | - pip install meson-python ninja cython cmake + pip install meson-python ninja "cython>=3.1.0" cmake pip install ${{ matrix.numpy_version }} - name: List oneAPI folder content diff --git a/.github/workflows/build-with-standard-clang.yml b/.github/workflows/build-with-standard-clang.yml index 97c1d48b..39b59501 100644 --- a/.github/workflows/build-with-standard-clang.yml +++ b/.github/workflows/build-with-standard-clang.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] numpy_version: ["'numpy>=2'"] env: @@ -47,7 +47,7 @@ jobs: - name: Install mkl_random dependencies run: | - pip install meson-python ninja cmake cython mkl-devel + pip install meson-python ninja cmake "cython>=3.1.0" mkl-devel pip install ${{ matrix.numpy_version }} - name: Build mkl_random diff --git a/.github/workflows/build_pip.yml b/.github/workflows/build_pip.yml index 798b1ff7..43062e4d 100644 --- a/.github/workflows/build_pip.yml +++ b/.github/workflows/build_pip.yml @@ -17,8 +17,21 @@ jobs: strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13"] use_pre: ["", "--pre"] + include: + - python: "3.14" + python_spec: "3.14.* *_cp314" + use_pre: "" + - python: "3.14" + python_spec: "3.14.* *_cp314" + use_pre: "--pre" + - python: "3.14" + python_spec: "3.14.* *_cp314t" + use_pre: "" + - python: "3.14" + python_spec: "3.14.* *_cp314t" + use_pre: "--pre" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -34,11 +47,11 @@ jobs: - name: Install MKL run: | - conda install mkl-devel + conda install mkl-devel "python=${{ matrix.python_spec || matrix.python }}" - name: Build with pip run: | - pip install --no-cache-dir meson-python ninja cmake cython + pip install --no-cache-dir meson-python ninja cmake "cython>=3.1.0" pip install --no-cache-dir numpy ${{ matrix.use_pre }} pip install -e . --no-build-isolation --no-deps --verbose pip install --no-cache-dir pytest diff --git a/.github/workflows/conda-package-cf.yml b/.github/workflows/conda-package-cf.yml index 0fb666eb..516e1b09 100644 --- a/.github/workflows/conda-package-cf.yml +++ b/.github/workflows/conda-package-cf.yml @@ -30,6 +30,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" steps: - name: Cancel Previous Runs @@ -52,9 +58,9 @@ jobs: with: path: ~/.conda/pkgs key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('**/meta.yaml') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Add conda to system path @@ -66,7 +72,7 @@ jobs: - name: Build conda package run: | CHANNELS=(-c conda-forge --override-channels) - VERSIONS=(--python "${{ matrix.python }}" --numpy "${{ matrix.numpy }}") + VERSIONS=(--python "${{ matrix.python_spec || matrix.python }}" --numpy "${{ matrix.numpy }}") TEST=(--no-test) conda build \ @@ -78,7 +84,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} path: /usr/share/miniconda/conda-bld/linux-64/${{ env.PACKAGE_NAME }}-*.conda build_windows: @@ -99,6 +105,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" env: conda-bld: C:\Miniconda\conda-bld\win-64\ @@ -127,9 +139,9 @@ jobs: with: path: /home/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('**/meta.yaml') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Store conda paths as envs @@ -143,12 +155,12 @@ jobs: conda list -n base - name: Build conda package - run: conda build --no-test --python ${{ matrix.python }} --numpy ${{ matrix.numpy }} -c conda-forge --override-channels conda-recipe-cf + run: conda build --no-test --python "${{ matrix.python_spec || matrix.python }}" --numpy ${{ matrix.numpy }} -c conda-forge --override-channels conda-recipe-cf - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda test_linux: @@ -168,6 +180,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" env: CHANNELS: -c conda-forge --override-channels @@ -176,7 +194,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} - name: Add conda to system path run: echo "$CONDA/bin" >> "$GITHUB_PATH" @@ -199,7 +217,7 @@ jobs: . "$CONDA/etc/profile.d/conda.sh" PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels --only-deps --dry-run > lockfile + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python_spec || matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels --only-deps --dry-run > lockfile cat lockfile - name: Set pkgs_dirs @@ -213,9 +231,9 @@ jobs: with: path: ~/.conda/pkgs key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('lockfile') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Install mkl_random @@ -223,7 +241,7 @@ jobs: . "$CONDA/etc/profile.d/conda.sh" PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python_spec || matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels # Test installed packages conda list @@ -252,6 +270,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" env: CHANNELS: -c conda-forge --override-channels @@ -260,7 +284,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} - uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 with: @@ -292,7 +316,7 @@ jobs: FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( SET PACKAGE_VERSION=%%F ) - conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% python=${{ matrix.python }} numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile + conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile - name: Cache conda packages uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -301,9 +325,9 @@ jobs: with: path: /home/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('lockfile') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Install mkl_random @@ -315,7 +339,7 @@ jobs: FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( SET PACKAGE_VERSION=%%F ) - conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% pytest python=${{ matrix.python }} numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} + conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% pytest "python=${{ matrix.python_spec || matrix.python }}" numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} # Test installed packages conda list @@ -339,6 +363,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" steps: - name: Cancel Previous Runs @@ -364,9 +394,9 @@ jobs: with: path: /Users/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('**/meta.yaml') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Install conda-build @@ -384,7 +414,7 @@ jobs: shell: bash -el {0} run: | CHANNELS=(-c conda-forge --override-channels) - VERSIONS=(--python "${{ matrix.python }}" --numpy "${{ matrix.numpy }}") + VERSIONS=(--python "${{ matrix.python_spec || matrix.python }}" --numpy "${{ matrix.numpy }}") TEST=(--no-test) conda build \ @@ -396,7 +426,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda test_osx: @@ -415,12 +445,18 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" steps: - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} - uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 with: @@ -448,7 +484,7 @@ jobs: run: | PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels --only-deps --dry-run > lockfile + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python_spec || matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels --only-deps --dry-run > lockfile cat lockfile - name: Cache conda packages @@ -458,9 +494,9 @@ jobs: with: path: /Users/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('lockfile') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Install mkl_random @@ -468,7 +504,7 @@ jobs: run: | PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python_spec || matrix.python }}" "numpy=${{ matrix.numpy }}" -c "$GITHUB_WORKSPACE/channel" -c conda-forge --override-channels # Test installed packages conda list -n "${{ env.TEST_ENV_NAME }}" diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index a9333f6c..cef4892a 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -32,6 +32,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" steps: - name: Cancel Previous Runs @@ -54,9 +60,9 @@ jobs: with: path: ~/.conda/pkgs key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('**/meta.yaml') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Add conda to system path @@ -85,7 +91,7 @@ jobs: - name: Build conda package run: | CHANNELS=(-c https://software.repos.intel.com/python/conda -c conda-forge --override-channels) - VERSIONS=(--python "${{ matrix.python }}" --numpy "${{ matrix.numpy }}") + VERSIONS=(--python "${{ matrix.python_spec || matrix.python }}" --numpy "${{ matrix.numpy }}") TEST=(--no-test) conda build \ @@ -97,7 +103,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda build_windows: @@ -118,6 +124,12 @@ jobs: numpy: "2.3" - python: "3.14" numpy: "2.3" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + - python: "3.14" + numpy: "2.3" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" steps: - name: Cancel Previous Runs @@ -145,9 +157,9 @@ jobs: with: path: /home/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('**/meta.yaml') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Store conda paths as envs @@ -164,12 +176,12 @@ jobs: conda list -n base - name: Build conda package - run: conda build --no-test --python "${{ matrix.python }}" --numpy "${{ matrix.numpy }}" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels conda-recipe + run: conda build --no-test --python "${{ matrix.python_spec || matrix.python }}" --numpy "${{ matrix.numpy }}" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels conda-recipe - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda test_linux: @@ -178,9 +190,20 @@ jobs: strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13"] numpy: ['numpy">=2"'] experimental: [false] + include: + - python: "3.14" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + numpy: 'numpy">=2"' + experimental: false + - python: "3.14" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" + numpy: 'numpy">=2"' + experimental: false env: CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels @@ -189,7 +212,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} - name: Add conda to system path run: echo "$CONDA/bin" >> "$GITHUB_PATH" @@ -225,7 +248,7 @@ jobs: . "$CONDA/etc/profile.d/conda.sh" PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python }}" ${{ matrix.numpy }} -c "$GITHUB_WORKSPACE/channel" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels --only-deps --dry-run > lockfile + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c "$GITHUB_WORKSPACE/channel" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels --only-deps --dry-run > lockfile cat lockfile - name: Set pkgs_dirs @@ -239,9 +262,9 @@ jobs: with: path: ~/.conda/pkgs key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('lockfile') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- - name: Install mkl_random @@ -249,7 +272,7 @@ jobs: . "$CONDA/etc/profile.d/conda.sh" PACKAGE_VERSION="$(python -c "${VER_SCRIPT1} ${VER_SCRIPT2}")" export PACKAGE_VERSION - conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python }}" ${{ matrix.numpy }} -c "$GITHUB_WORKSPACE/channel" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels + conda create -n "${{ env.TEST_ENV_NAME }}" "${PACKAGE_NAME}=${PACKAGE_VERSION}" pytest "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c "$GITHUB_WORKSPACE/channel" -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels conda activate "${{ env.TEST_ENV_NAME }}" # Test installed packages @@ -267,9 +290,20 @@ jobs: strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13"] numpy: ['numpy">=2"'] experimental: [false] + include: + - python: "3.14" + python_spec: "3.14.* *_cp314" + python_tag: "3.14" + numpy: 'numpy">=2"' + experimental: false + - python: "3.14" + python_spec: "3.14.* *_cp314t" + python_tag: "3.14t" + numpy: 'numpy">=2"' + experimental: false env: CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels @@ -278,7 +312,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} + name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }} - uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 with: @@ -330,7 +364,7 @@ jobs: FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( SET PACKAGE_VERSION=%%F ) - conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% python=${{ matrix.python }} ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile + conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile - name: Cache conda packages uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -339,9 +373,9 @@ jobs: with: path: /home/runner/conda_pkgs_dir key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}-${{hashFiles('lockfile') }} restore-keys: | - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_tag || matrix.python }}- ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- # add intel-openmp as an explicit dependency @@ -356,7 +390,7 @@ jobs: SET PACKAGE_VERSION=%%F ) SET "WORKAROUND_DEPENDENCIES=intel-openmp" - conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% %WORKAROUND_DEPENDENCIES% pytest python=${{ matrix.python }} ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} + conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% %WORKAROUND_DEPENDENCIES% pytest "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} conda activate ${{ env.TEST_ENV_NAME }} # Test installed packages From 9b41e87a9e0c5f4a7f442be952a3b10a7801b4a9 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 04:20:41 -0700 Subject: [PATCH 04/10] Add free-threading test for mkl_random --- mkl_random/tests/test_freethreading.py | 129 +++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 mkl_random/tests/test_freethreading.py diff --git a/mkl_random/tests/test_freethreading.py b/mkl_random/tests/test_freethreading.py new file mode 100644 index 00000000..d5c4a5ba --- /dev/null +++ b/mkl_random/tests/test_freethreading.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026, Intel Corporation +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Intel Corporation nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import sys +import sysconfig +import threading + +import numpy as np +import pytest + +# Oversubscription: MKL spawns its own thread pool per calling thread, so +# generating from many Python threads concurrently can spawn far more OS +# threads than cores. Cap it before mkl_random/MKL initialize. +os.environ.setdefault("MKL_NUM_THREADS", "1") + +import mkl_random # noqa: E402 + +FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + +def test_concurrent_sampling_per_instance(): + # Each thread owns a private MKLRandomState seeded identically, so the + # per-instance lock + `nogil` sampling must reproduce the single-threaded + # result exactly regardless of concurrency. + n_threads = 4 + size = 10**5 + 1 # large enough that per-thread nogil sampling overlaps + seed = 1234 + + expected = mkl_random.MKLRandomState(seed).normal(size=size) + + results = [None] * n_threads + errors = [] + + def worker(i): + try: + rs = mkl_random.MKLRandomState(seed) + results[i] = rs.normal(size=size) + except Exception as exc: # pylint: disable=broad-except + errors.append(exc) + + threads = [ + threading.Thread(target=worker, args=(i,)) for i in range(n_threads) + ] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors + + for i in range(n_threads): + np.testing.assert_array_equal(results[i], expected) + + +def test_concurrent_shared_singleton(): + # Module-level functions share a single lock-guarded RandomState. Hammering + # it from many threads must not corrupt state, crash, or return garbage. + n_threads = 8 + size = 10**5 + 1 + results = [None] * n_threads + errors = [] + + def worker(i): + try: + results[i] = mkl_random.uniform(size=size) + except Exception as exc: # pylint: disable=broad-except + errors.append(exc) + + threads = [ + threading.Thread(target=worker, args=(i,)) for i in range(n_threads) + ] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors + + for i in range(n_threads): + assert results[i].shape == (size,) + assert np.all(np.isfinite(results[i])) + + +def test_concurrent_patch_restore(): + n_threads = 8 + n_iters = 20 + + def worker(): + for _ in range(n_iters): + mkl_random.patch_numpy_random() + mkl_random.restore_numpy_random() + + threads = [threading.Thread(target=worker) for _ in range(n_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not mkl_random.is_patched() + + +@pytest.mark.skipif( + not FREE_THREADED, reason="requires a free-threaded CPython build" +) +def test_gil_not_reenabled_on_import(): + assert not sys._is_gil_enabled() # pylint: disable=no-member From 4de50ebc1e12fbb77c73ab0d9289569a7141c7b4 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 04:21:46 -0700 Subject: [PATCH 05/10] Update README build deps to cython>=3.1.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e5f06a8..ce6d04ba 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ If you already have `mkl` and `numpy` installed (from your system or a conda env and want to reuse them instead of pulling fresh copies into an isolated build, first install the build dependencies: ```sh -pip install meson-python cmake ninja cython numpy mkl-devel +pip install meson-python cmake ninja "cython>=3.1.0" numpy mkl-devel ``` then build against the existing installation with: From 8e5a8e5885b2bb3e514c927fa3f9a084c590ad9e Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 04:57:37 -0700 Subject: [PATCH 06/10] Add a missing continue-on-error to conda-package.yml --- .github/workflows/conda-package.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index cef4892a..2334fb27 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -204,6 +204,7 @@ jobs: python_tag: "3.14t" numpy: 'numpy">=2"' experimental: false + continue-on-error: ${{ matrix.experimental }} env: CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels @@ -304,6 +305,7 @@ jobs: python_tag: "3.14t" numpy: 'numpy">=2"' experimental: false + continue-on-error: ${{ matrix.experimental }} env: CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels From 4b9cfe7111f36eb11de4887ff4ad1f404802504f Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 05:04:59 -0700 Subject: [PATCH 07/10] Add gh-159 to CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eff67b71..52eb980b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # [dev] (MM/DD/YYYY) ### Added +* Added support for free-threaded (GIL-disabled) CPython builds: the Cython extension is compiled with `freethreading_compatible=True`, so importing `mkl_random` no longer re-enables the GIL [gh-159](https://github.com/IntelPython/mkl_random/pull/159) ### Changed +* Raised the minimum build-time `Cython` requirement to `3.1.0`, the first release providing the `freethreading_compatible` directive [gh-159](https://github.com/IntelPython/mkl_random/pull/159) + +### Removed +* Removed the `python-gil` constraint from the conda recipes, which pinned `mkl_random` to GIL-enabled Python 3.14 builds [gh-159](https://github.com/IntelPython/mkl_random/pull/159) ### Fixed From be5c348130ec8ac5784ef42d9e3f3ae082a9f73f Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 12 Aug 2026 13:51:52 -0700 Subject: [PATCH 08/10] Create fresh Windows test env for cp314t --- .github/workflows/conda-package-cf.yml | 5 ++--- .github/workflows/conda-package.yml | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/conda-package-cf.yml b/.github/workflows/conda-package-cf.yml index 516e1b09..4590abcc 100644 --- a/.github/workflows/conda-package-cf.yml +++ b/.github/workflows/conda-package-cf.yml @@ -290,8 +290,7 @@ jobs: with: miniforge-version: latest channels: conda-forge - activate-environment: ${{ env.TEST_ENV_NAME }} - python-version: ${{ matrix.python }} + activate-environment: base - name: Install conda-index run: | @@ -316,7 +315,7 @@ jobs: FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( SET PACKAGE_VERSION=%%F ) - conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile + conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" numpy=${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile - name: Cache conda packages uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index 2334fb27..946324ba 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -318,11 +318,9 @@ jobs: - uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 with: - auto-update-conda: true miniforge-version: latest channels: conda-forge - activate-environment: ${{ env.TEST_ENV_NAME }} - python-version: ${{ matrix.python }} + activate-environment: base - name: Install conda-index run: | @@ -366,7 +364,7 @@ jobs: FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( SET PACKAGE_VERSION=%%F ) - conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile + conda create -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% "python=${{ matrix.python_spec || matrix.python }}" ${{ matrix.numpy }} -c ${{ env.GITHUB_WORKSPACE }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile - name: Cache conda packages uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 From 33e696b0365dbea7a050e8ea31567d72e3193837 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 7 Sep 2026 04:09:44 -0700 Subject: [PATCH 09/10] Fix data races in unguarded RNG methods under free-threading --- mkl_random/mklrand.pyx | 92 +++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/mkl_random/mklrand.pyx b/mkl_random/mklrand.pyx index f0a2a5e9..c4491c1a 100644 --- a/mkl_random/mklrand.pyx +++ b/mkl_random/mklrand.pyx @@ -1632,13 +1632,13 @@ cdef class _MKLRandomState: cdef unsigned int stream_id cdef cnp.ndarray obj "arrayObject_obj" - if (brng): - brng_token, stream_id = _parse_brng_argument(brng) - else: - brng_token = irk_get_brng_and_stream_mkl( - self.internal_state, &stream_id - ) with self.lock: + if (brng): + brng_token, stream_id = _parse_brng_argument(brng) + else: + brng_token = irk_get_brng_and_stream_mkl( + self.internal_state, &stream_id + ) try: if seed is None: _errcode = irk_randomseed_mkl( @@ -6599,7 +6599,8 @@ cdef class _MKLRandomState: raise ValueError("n < 0") # numpy#20483: Avoids divide by 0 niter = sz // d if d else 0 - irk_multinomial_vec(self.internal_state, niter, mnix, n, d, pix) + with self.lock, nogil: + irk_multinomial_vec(self.internal_state, niter, mnix, n, d, pix) return multin @@ -6976,7 +6977,8 @@ cdef class MKLRandomState(_MKLRandomState): """ cdef int err, brng_id - err = irk_leapfrog_stream_mkl(self.internal_state, k, nstreams) + with self.lock: + err = irk_leapfrog_stream_mkl(self.internal_state, k, nstreams) if err == -1: raise ValueError("The stream state buffer is corrupted") @@ -6997,7 +6999,8 @@ cdef class MKLRandomState(_MKLRandomState): """ cdef int err, brng_id - err = irk_skipahead_stream_mkl(self.internal_state, nskips) + with self.lock: + err = irk_skipahead_stream_mkl(self.internal_state, nskips) if err == -1: raise ValueError("The stream state buffer is corrupted") @@ -7080,9 +7083,10 @@ cdef class MKLRandomState(_MKLRandomState): if (( lo) == lo) and ((hi) == hi): if size is None: - irk_discrete_uniform_vec( - self.internal_state, 1, &rv_int, lo, hi - ) + with self.lock, nogil: + irk_discrete_uniform_vec( + self.internal_state, 1, &rv_int, lo, hi + ) return rv_int else: array = np.empty(size, np.int32) @@ -7099,9 +7103,10 @@ cdef class MKLRandomState(_MKLRandomState): return array else: if size is None: - irk_discrete_uniform_long_vec( - self.internal_state, 1, &rv_long, lo, hi - ) + with self.lock, nogil: + irk_discrete_uniform_long_vec( + self.internal_state, 1, &rv_long, lo, hi + ) return rv_long else: array = np.empty(size, int) @@ -7284,35 +7289,38 @@ cdef class MKLRandomState(_MKLRandomState): method, [ICDF, BOXMULLER2, BOXMULLER], _method_alias_dict_gaussian ) if (method is ICDF): - irk_multinormal_vec_ICDF( - self.internal_state, - n, - res_data, - dim, - mean_data, - t_data, - storage_mode - ) + with self.lock, nogil: + irk_multinormal_vec_ICDF( + self.internal_state, + n, + res_data, + dim, + mean_data, + t_data, + storage_mode + ) elif (method is BOXMULLER2): - irk_multinormal_vec_BM2( - self.internal_state, - n, - res_data, - dim, - mean_data, - t_data, - storage_mode - ) + with self.lock, nogil: + irk_multinormal_vec_BM2( + self.internal_state, + n, + res_data, + dim, + mean_data, + t_data, + storage_mode + ) else: - irk_multinormal_vec_BM1( - self.internal_state, - n, - res_data, - dim, - mean_data, - t_data, - storage_mode - ) + with self.lock, nogil: + irk_multinormal_vec_BM1( + self.internal_state, + n, + res_data, + dim, + mean_data, + t_data, + storage_mode + ) return resarr From 5d864420119b0606e33e664e647fcb191627fdfe Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 7 Sep 2026 04:30:18 -0700 Subject: [PATCH 10/10] Add shared-stream race regression test for free-threading --- mkl_random/tests/test_freethreading.py | 93 +++++++++++++++----------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/mkl_random/tests/test_freethreading.py b/mkl_random/tests/test_freethreading.py index d5c4a5ba..a2aaa3d1 100644 --- a/mkl_random/tests/test_freethreading.py +++ b/mkl_random/tests/test_freethreading.py @@ -41,28 +41,18 @@ FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) -def test_concurrent_sampling_per_instance(): - # Each thread owns a private MKLRandomState seeded identically, so the - # per-instance lock + `nogil` sampling must reproduce the single-threaded - # result exactly regardless of concurrency. - n_threads = 4 - size = 10**5 + 1 # large enough that per-thread nogil sampling overlaps - seed = 1234 - - expected = mkl_random.MKLRandomState(seed).normal(size=size) - - results = [None] * n_threads +def _run_on_threads(worker, n_threads): + # Run worker(i) on n_threads and fail if any thread raised. errors = [] - def worker(i): + def wrapped(i): try: - rs = mkl_random.MKLRandomState(seed) - results[i] = rs.normal(size=size) + worker(i) except Exception as exc: # pylint: disable=broad-except errors.append(exc) threads = [ - threading.Thread(target=worker, args=(i,)) for i in range(n_threads) + threading.Thread(target=wrapped, args=(i,)) for i in range(n_threads) ] for t in threads: t.start() @@ -71,8 +61,24 @@ def worker(i): assert not errors - for i in range(n_threads): - np.testing.assert_array_equal(results[i], expected) + +def test_concurrent_sampling_per_instance(): + # Each thread owns a private MKLRandomState seeded identically, so the + # per-instance lock + `nogil` sampling must reproduce the single-threaded + # result exactly regardless of concurrency. + n_threads = 4 + size = 10**5 + 1 # large enough that per-thread nogil sampling overlaps + seed = 1234 + expected = mkl_random.MKLRandomState(seed).normal(size=size) + results = [None] * n_threads + + def worker(i): + results[i] = mkl_random.MKLRandomState(seed).normal(size=size) + + _run_on_threads(worker, n_threads) + + for r in results: + np.testing.assert_array_equal(r, expected) def test_concurrent_shared_singleton(): @@ -81,47 +87,54 @@ def test_concurrent_shared_singleton(): n_threads = 8 size = 10**5 + 1 results = [None] * n_threads - errors = [] def worker(i): - try: - results[i] = mkl_random.uniform(size=size) - except Exception as exc: # pylint: disable=broad-except - errors.append(exc) + results[i] = mkl_random.uniform(size=size) - threads = [ - threading.Thread(target=worker, args=(i,)) for i in range(n_threads) - ] - for t in threads: - t.start() - for t in threads: - t.join() + _run_on_threads(worker, n_threads) - assert not errors - - for i in range(n_threads): - assert results[i].shape == (size,) - assert np.all(np.isfinite(results[i])) + for r in results: + assert r.shape == (size,) + assert np.all(np.isfinite(r)) def test_concurrent_patch_restore(): n_threads = 8 n_iters = 20 - def worker(): + def worker(_i): for _ in range(n_iters): mkl_random.patch_numpy_random() mkl_random.restore_numpy_random() - threads = [threading.Thread(target=worker) for _ in range(n_threads)] - for t in threads: - t.start() - for t in threads: - t.join() + _run_on_threads(worker, n_threads) assert not mkl_random.is_patched() +def test_concurrent_multinormal_cholesky_shared(): + # A data race on the shared stream reuses values; assert few duplicates. + n_threads = 8 + mean = np.zeros(1) + ch = np.eye(1) + rng = mkl_random.MKLRandomState(12345) + chunks = [None] * n_threads + + def worker(i): + parts = [ + rng.multinormal_cholesky(mean, ch, size=4000).ravel() + for _ in range(25) + ] + chunks[i] = np.concatenate(parts) + + _run_on_threads(worker, n_threads) + + allvals = np.concatenate(chunks) + assert np.all(np.isfinite(allvals)) + dup_frac = 1.0 - np.unique(allvals).size / allvals.size + assert dup_frac < 0.01, f"shared stream corrupted: {dup_frac:.3%} dups" + + @pytest.mark.skipif( not FREE_THREADED, reason="requires a free-threaded CPython build" )