Skip to content

build: default to a parallel extension build in setup.py - #565

Merged
hwanseoc merged 1 commit into
NVIDIA:developfrom
hwanseoc:fix/setup-py-default-parallel-build
Aug 12, 2026
Merged

build: default to a parallel extension build in setup.py#565
hwanseoc merged 1 commit into
NVIDIA:developfrom
hwanseoc:fix/setup-py-default-parallel-build

Conversation

@hwanseoc

@hwanseoc hwanseoc commented Aug 12, 2026

Copy link
Copy Markdown
Member

Problem

CMakeBuild.build_extension only passes -j to cmake --build when self.parallel is set. Neither pip nor PyPA-build sets it, so unless the user exports CMAKE_BUILD_PARALLEL_LEVEL, the default pip install . / pip install -e . path builds the pybind11 extension one translation unit at a time.

Fix

Fall back to -j min(os.cpu_count(), 8) when neither CMAKE_BUILD_PARALLEL_LEVEL nor self.parallel is set.

The cap is deliberate: the extension is ~6 TUs and each peaks at 1-2 GB of compiler memory, so a larger -j buys no additional throughput and only raises peak memory on many-core machines.

Precedence is unchanged — CMAKE_BUILD_PARALLEL_LEVEL still short-circuits the whole block, and an explicit build_ext -j still wins over the new default.

Test

  • pip install -e . now compiles the extension in parallel instead of serially.
  • CMAKE_BUILD_PARALLEL_LEVEL=2 pip install -e . and python setup.py build_ext -j4 behave as before.

Summary by CodeRabbit

  • Performance
    • Builds now automatically use available CPU resources for parallel compilation when no parallelism setting is specified.
    • Parallel build jobs are capped at eight to help maintain consistent build performance.

pip and PyPA-build never set build_ext's -j, so `cmake --build` fell
through with no parallel flag and compiled the pybind11 extension one
translation unit at a time. Default to -j min(cpu_count, 8) when neither
CMAKE_BUILD_PARALLEL_LEVEL nor self.parallel is set.

The cap is deliberate: the extension is ~6 TUs and each peaks at 1-2 GB
of compiler memory, so a larger -j buys no additional throughput and
only raises peak memory on many-core machines. Explicit
CMAKE_BUILD_PARALLEL_LEVEL and `build_ext -j` continue to take
precedence.
@hwanseoc hwanseoc added cat-infra Build, packaging, tooling, dependency, release, or repository maintenance work. mod-infra Infrastructure, CI/CD, build systems, packaging, releases, or repo maintenance. mod-frontend cuDNN frontend APIs, operation graph construction, plans, and user-facing wrappers. orig-nv-eng Reported or requested by NVIDIA engineering. labels Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The build configuration now adds a CMake -j value based on available CPUs, capped at 8, when no explicit parallel setting is provided. Explicit self.parallel behavior remains unchanged.

Changes

Build Configuration

Layer / File(s) Summary
Automatic parallel compilation
setup.py
Builds without an explicit parallel setting use the lesser of the available CPU count and 8 for CMake parallel compilation. Explicit self.parallel values remain unchanged.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Suggested reviewers: anerudhan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the problem, fix, precedence, rationale, and testing, but it omits several template sections and checklist items.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling a default parallel extension build in setup.py.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@hwanseoc
hwanseoc requested a review from vedaanta August 12, 2026 20:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@setup.py`:
- Around line 97-100: Update the comment near the fallback build-parallelism
logic to accurately describe Ninja’s default parallel execution and the explicit
-jmin(os.cpu_count() or 1, 8) cap when CMAKE_BUILD_PARALLEL_LEVEL and
self.parallel are unset. Add focused coverage verifying this fallback
concurrency value and its cap at 8.
- Around line 96-101: Update the build_ext parallel-argument handling so the
automatic CMake job count is used only when self.parallel is None; when
self.parallel is explicitly 0, append CMake’s single-job setting -j1 to preserve
serial builds. Add regression coverage for explicit build_ext -j 0 and retain
existing behavior for other parallel values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 57f38e81-d157-4d40-9e06-962dd84ea831

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6b201 and a7767f7.

📒 Files selected for processing (1)
  • setup.py

Comment thread setup.py
Comment on lines +96 to +101
else:
# Without an explicit -j, `cmake --build` compiles one TU at a
# time, so pip installs built the extension serially. Cap at 8:
# the extension has ~6 TUs, each peaking at 1-2 GB of compiler
# memory, so higher values buy nothing and only add pressure.
build_args += [f"-j{min(os.cpu_count() or 1, 8)}"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

if rg -n 'if hasattr\(self, "parallel"\) and self\.parallel:' setup.py; then
  echo "The truthiness check still treats explicit -j 0 as unset." >&2
  exit 1
fi

rg -n --glob '*test*.py' 'parallel\s*=\s*0|build_ext.*-j.*0' . || {
  echo "Add regression coverage for explicit -j 0." >&2
  exit 1
}

Repository: NVIDIA/cudnn-frontend

Length of output: 280


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- setup.py relevant section ---'
sed -n '70,115p' setup.py

printf '%s\n' '--- project configuration ---'
for f in pyproject.toml setup.cfg; do
  if [ -f "$f" ]; then
    echo "[$f]"
    sed -n '1,180p' "$f"
  fi
done

printf '%s\n' '--- parallel-related code and tests ---'
rg -n --glob '*.py' --glob '*.toml' --glob '*.cfg' \
  'self\.parallel|parallel\s*=|build_ext|cmake --build|CMAKE_BUILD_PARALLEL_LEVEL|-j[[:space:]]*0' .

printf '%s\n' '--- test files ---'
git ls-files '*test*.py' '*tests*' | sed -n '1,160p'

Repository: NVIDIA/cudnn-frontend

Length of output: 15213


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
import inspect
from setuptools.command.build_ext import build_ext

print("--- setuptools build_ext option definition ---")
print(inspect.getsource(build_ext.user_options))
print("--- setuptools build_ext parallel handling ---")
source = inspect.getsource(build_ext)
for i, line in enumerate(source.splitlines(), 1):
    if "parallel" in line or "ThreadPool" in line or "build_extensions" in line:
        start = max(1, i - 4)
        end = min(len(source.splitlines()), i + 8)
        print(f"\n[source lines {start}-{end}]")
        print("\n".join(f"{n}: {source.splitlines()[n-1]}" for n in range(start, end + 1)))
PY

printf '%s\n' '--- installed tool versions ---'
cmake --version 2>/dev/null | head -n 1 || true
ninja --version 2>/dev/null || true

printf '%s\n' '--- setup.py generator and build command context ---'
sed -n '1,110p' setup.py

Repository: NVIDIA/cudnn-frontend

Length of output: 282


🌐 Web query:

CMake --build -j 0 Ninja semantics official documentation

💡 Result:

There is no official "0" flag or specific "0" semantic defined for the cmake --build command [1][2][3]. When invoking cmake --build, the command acts as a wrapper that abstracts the native build tool's interface [1][4]. If you are seeing a reference to 0 in this context, it is likely one of the following: 1. Return Code: Upon regular successful termination, the cmake executable (and the underlying build tool) returns the exit code 0 [1][5]. 2. Positional Argument Confusion: The cmake --build command requires the binary directory as the first argument [1][2]. If a script or user provides 0 in that position, it may be interpreted as a literal directory name named "0" or result in an error if that directory does not exist. 3. Build Tool Misinterpretation: Some users may confuse shell return values with command-line arguments. To pass specific options to the Ninja build tool through CMake, you must use the double-dash (--) separator [1][4][3]. Any options placed after the -- are passed directly to the underlying generator (e.g., Ninja) [1][3]. For example: cmake --build. -- -v This command instructs CMake to build the project in the current directory and passes the -v (verbose) flag directly to the Ninja generator [6]. There is no official command-line option "0" for Ninja or CMake build operations [1][4].

Citations:


🌐 Web query:

setuptools build_ext --parallel 0 build_extensions source

💡 Result:

The command 'setuptools build_ext --parallel 0 build_extensions source' contains a misunderstanding of how the 'build_ext' command works in 'setuptools'. 1. Command Structure: The 'build_ext' command accepts a '--parallel' (or '-j') option to specify the number of parallel jobs for building C/C++ extensions [1][2][3]. It does not take a 'source' argument at the end, nor does it recognize a 'build_extensions' argument in that position [4][3]. 2. Parallel Flag Behavior: The '--parallel' option expects an integer representing the number of parallel workers [1][3]. If set to '0', it typically does not disable parallelism; rather, the underlying logic often defaults to serial execution if the value is not a positive integer or if it falls back to a serial implementation [4][1][3]. If you want to enable parallel building, you should pass a positive integer (e.g., --parallel 4) or omit the number to let 'setuptools' attempt to use the number of available CPUs [4][1][3]. 3. How Parallelization Works: 'setuptools' parallelizes the build process at the extension level, meaning it builds separate extension modules concurrently, not individual source files within a single extension (unlike a Makefile) [5]. If you are trying to build extensions during a package installation (e.g., via 'pip'), you cannot always pass flags directly to 'build_ext' [6][7]. Instead, common workarounds include: - Setting the 'DIST_EXTRA_CONFIG' environment variable pointing to a file containing '[build_ext] parallel = 8' [6][7]. - Using environment variables if supported by specific configurations [6][2]. - Using 'setup.cfg' to permanently set 'parallel = N' under the '[build_ext]' section [6]. In summary, 'build_ext --parallel [N]' is the correct syntax for direct 'setup.py' calls [7][2], but 'source' and 'build_extensions' are not valid arguments for this command [4][3].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- installed tool versions ---'
cmake --version 2>/dev/null | head -n 1 || true
ninja --version 2>/dev/null || true

printf '%s\n' '--- setuptools source references ---'
curl -fsSL \
  https://raw.githubusercontent.com/pypa/setuptools/v64.0.0/setuptools/_distutils/command/build_ext.py \
  | grep -n -A18 -B8 -E 'parallel|build_extensions' || true

printf '%s\n' '--- CMake documentation references ---'
curl -fsSL \
  https://raw.githubusercontent.com/Kitware/CMake/master/Help/manual/cmake.1.rst \
  | grep -n -A12 -B8 -E 'parallel|--parallel|-j' | head -n 100 || true

Repository: NVIDIA/cudnn-frontend

Length of output: 10564


🌐 Web query:

site:cmake.org/cmake/help/latest/manual/cmake.1.html cmake --build --parallel 0

💡 Result:

The --parallel option (also represented as -j) for the cmake --build command is used to specify the maximum number of concurrent processes for the build [1]. According to the official documentation, if the argument for the number of jobs is omitted, the native build tool uses its own default [1]. While the documentation notes that a value of 1 can be used to limit the build to a single job, it does not explicitly define the behavior of passing 0 as an argument [1]. Typically, setting a parallel job count to 0 in build systems can result in an error, be ignored, or default to a specific behavior like using the number of available CPU cores, but this specific behavior is not documented for the CMake build command [1].

Citations:


🌐 Web query:

site:ninja-build.org ninja -j 0 parallel jobs

💡 Result:

In the Ninja build system, you do not need to explicitly specify a number of parallel jobs to achieve parallel execution. Ninja is designed to run builds in parallel by default, automatically determining the number of concurrent jobs based on the number of CPUs available on your system [1][2]. If you wish to override this default behavior and specify a fixed number of parallel jobs, you can use the -j flag [1][2]. For example, running the command ninja -j 20 will instruct Ninja to execute up to 20 build commands in parallel [1][2]. Additionally, Ninja supports the GNU Make jobserver client protocol (as of version 1.13), which allows it to coordinate parallelism with other build systems or jobserver pools [1][2]. Regardless of any custom pool configurations defined within your build files, Ninja will never exceed the parallelism level set by the default auto-detection or the explicit limit provided by the -j flag [1][2].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

source = Path("setup.py").read_text()

assert 'if hasattr(self, "parallel") and self.parallel:' in source
assert 'build_args += [f"-j{min(os.cpu_count() or 1, 8)}"]' in source
assert 'cmake_args += ["-GNinja"' in source
assert 'ninja==1.11.1.1' in Path("pyproject.toml").read_text()

def current_build_args(parallel, cpu_count=16):
    if parallel:
        return [f"-j{parallel}"]
    return [f"-j{min(cpu_count or 1, 8)}"]

def proposed_build_args(parallel, cpu_count=16):
    if parallel is not None:
        return [f"-j{parallel}"] if parallel else []
    return [f"-j{min(cpu_count or 1, 8)}"]

print("current self.parallel=0:", current_build_args(0))
print("proposed elif ... is None, self.parallel=0:", proposed_build_args(0))
print("serial CMake setting documented by CMake: -j1")
print("current self.parallel=None:", current_build_args(None))
print("proposed self.parallel=None:", proposed_build_args(None))

test_files = [
    p for p in Path(".").rglob("*.py")
    if "test" in p.name.lower() or "tests" in p.parts
]
matches = []
for path in test_files:
    text = path.read_text(errors="replace")
    if "parallel" in text or "build_ext" in text:
        matches.append(str(path))
print("test files mentioning parallel/build_ext:", matches)
PY

Repository: NVIDIA/cudnn-frontend

Length of output: 247


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

setup = Path("setup.py").read_text()
pyproject = Path("pyproject.toml").read_text()

assert 'if hasattr(self, "parallel") and self.parallel:' in setup
assert 'build_args += [f"-j{min(os.cpu_count() or 1, 8)}"]' in setup
assert '"-GNinja"' in setup
assert "ninja==1.11.1.1" in pyproject

def current_build_args(parallel, cpu_count=16):
    if parallel:
        return [f"-j{parallel}"]
    return [f"-j{min(cpu_count or 1, 8)}"]

def proposed_build_args(parallel, cpu_count=16):
    if parallel is not None:
        return [f"-j{parallel}"] if parallel else []
    return [f"-j{min(cpu_count or 1, 8)}"]

print("current self.parallel=0:", current_build_args(0))
print("proposed elif ... is None, self.parallel=0:", proposed_build_args(0))
print("serial CMake setting: -j1")
print("current self.parallel=None:", current_build_args(None))
print("proposed self.parallel=None:", proposed_build_args(None))

matches = []
for path in Path(".").rglob("*.py"):
    if "test" not in path.name.lower() and "tests" not in path.parts:
        continue
    text = path.read_text(errors="replace")
    if "parallel" in text or "build_ext" in text:
        matches.append(str(path))
print("test files mentioning parallel/build_ext:", matches)
PY

Repository: NVIDIA/cudnn-frontend

Length of output: 426


Preserve serial behavior for explicit build_ext -j 0.

self.parallel == 0 selects setuptools’ serial extension path, but the current truthiness check applies the automatic CMake job count instead. Changing the condition to self.parallel is None alone would omit the CMake job option, while Ninja parallelizes by default. Map an explicit zero to CMake’s single-job setting, -j1, and add regression coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setup.py` around lines 96 - 101, Update the build_ext parallel-argument
handling so the automatic CMake job count is used only when self.parallel is
None; when self.parallel is explicitly 0, append CMake’s single-job setting -j1
to preserve serial builds. Add regression coverage for explicit build_ext -j 0
and retain existing behavior for other parallel values.

Comment thread setup.py
Comment on lines +97 to +100
# Without an explicit -j, `cmake --build` compiles one TU at a
# time, so pip installs built the extension serially. Cap at 8:
# the extension has ~6 TUs, each peaking at 1-2 GB of compiler
# memory, so higher values buy nothing and only add pressure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C3 'Ninja|cmake --build|CMAKE_BUILD_PARALLEL_LEVEL' setup.py pyproject.toml || true
rg -n -C3 'parallel|CMAKE_BUILD_PARALLEL_LEVEL|build_args' --glob '*test*' . || true

Repository: NVIDIA/cudnn-frontend

Length of output: 2894


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- setup.py ---'
sed -n '1,125p' setup.py

printf '%s\n' '--- pyproject.toml ---'
sed -n '1,180p' pyproject.toml

printf '%s\n' '--- build-related tests and references ---'
rg -n -C3 'build_ext|CMAKE_BUILD_PARALLEL_LEVEL|self\.parallel|build_args|Ninja|cmake --build' . \
  -g '*.py' -g '*.toml' -g '*.cfg' -g '*.ini' -g '*test*' || true

printf '%s\n' '--- repository status ---'
git diff --stat

Repository: NVIDIA/cudnn-frontend

Length of output: 12526


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

text = Path("setup.py").read_text()
start = text.index('        # Set CMAKE_BUILD_PARALLEL_LEVEL')
end = text.index('\n', text.index('                # the extension has', start)) + 1
print(text[start:end])

print("pyproject Ninja declarations:")
for line_no, line in enumerate(Path("pyproject.toml").read_text().splitlines(), 1):
    if "ninja" in line.lower() or "cmake" in line.lower():
        print(f"{line_no}: {line}")
PY

Repository: NVIDIA/cudnn-frontend

Length of output: 1023


🌐 Web query:

CMake cmake --build omitted --parallel default native build tool Ninja default parallelism documentation

💡 Result:

When using the cmake --build command, omitting the --parallel (or -j) flag instructs CMake to delegate parallelization decisions to the underlying native build tool [1][2]. The build tool then uses its own internal default behavior for determining the number of concurrent jobs [1][3]. In the case of the Ninja generator, Ninja defaults to running jobs in parallel based on the number of available CPU cores [4]. Specifically, Ninja historically defaults to N+2 parallel jobs, where N is the number of available processors [4]. If you wish to override this default behavior without relying on the native build tool's internal logic, you have two primary options: 1. Command-line flag: You can use the --parallel (or -j ) option with cmake --build to explicitly set the maximum number of concurrent processes [1][5]. 2. Environment variable: You can set the CMAKE_BUILD_PARALLEL_LEVEL environment variable [1][6]. If this variable is defined, CMake uses its value to specify the parallel level for the underlying build tool when the --parallel flag is not provided on the command line [6][3]. If the variable is defined but empty, CMake reverts to using the native build tool's default [6]. It is important to note that cmake --build is designed to be a cross-platform wrapper; when you omit --parallel, you are essentially telling CMake not to interfere with the build tool's default parallelism [1][3]. While Ninja's default is typically to use all available cores (plus extra for I/O waiting), other tools (like legacy Makefiles) may default to a single job unless configured otherwise, which is why the --parallel flag or CMAKE_BUILD_PARALLEL_LEVEL variable is often used to ensure consistent, predictable parallel performance across different environments [4][7].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

tree = ast.parse(Path("setup.py").read_text())
for node in ast.walk(tree):
    if isinstance(node, ast.If):
        source = ast.get_source_segment(Path("setup.py").read_text(), node) or ""
        if "CMAKE_BUILD_PARALLEL_LEVEL" in source:
            print(source)
            break

print("\nExpected fallback values:")
for cpu_count in (None, 1, 4, 8, 16):
    jobs = min(cpu_count or 1, 8)
    print(f"os.cpu_count()={cpu_count!r} -> -j{jobs}")
PY

Repository: NVIDIA/cudnn-frontend

Length of output: 1073


Correct the Ninja concurrency rationale.

CMake delegates omitted -j to the native build tool, and Ninja runs jobs in parallel by default. This fallback explicitly passes -jmin(os.cpu_count() or 1, 8) when CMAKE_BUILD_PARALLEL_LEVEL and self.parallel are unset. Update the comment and add focused coverage for this concurrency cap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setup.py` around lines 97 - 100, Update the comment near the fallback
build-parallelism logic to accurately describe Ninja’s default parallel
execution and the explicit -jmin(os.cpu_count() or 1, 8) cap when
CMAKE_BUILD_PARALLEL_LEVEL and self.parallel are unset. Add focused coverage
verifying this fallback concurrency value and its cap at 8.

@hwanseoc
hwanseoc merged commit 6f71b3e into NVIDIA:develop Aug 12, 2026
1 check passed
@Anerudhan Anerudhan mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat-infra Build, packaging, tooling, dependency, release, or repository maintenance work. mod-frontend cuDNN frontend APIs, operation graph construction, plans, and user-facing wrappers. mod-infra Infrastructure, CI/CD, build systems, packaging, releases, or repo maintenance. orig-nv-eng Reported or requested by NVIDIA engineering.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants