Skip to content

Commit fb74340

Browse files
authored
Migrate experimental MLIR backend (#346)
## Summary This is a curated migration of the experimental `numbast-mlir` backend into public Numbast under `numbast.experimental.mlir`, based on public `main`. - Adds the experimental MLIR backend package and its source-tree tests under `numbast/src/numbast/experimental/mlir`. - Keeps public top-level `numbast` APIs unchanged; MLIR APIs are imported from `numbast.experimental.mlir` and are not re-exported from top-level `numbast`. - Keeps `python -m numbast` as the static binding CLI entrypoint and routes `MLIR Backend: true` configs to the experimental MLIR static generator. - Extends the static binding config schema/docs with `MLIR Backend` and `Module Link Variables Used`. - Rejects `Module Link Variables Used` unless `MLIR Backend: true`. - Excludes the MLIR test subtree from existing broad CI/test commands. ## CI and test coverage Existing CI environments intentionally do not install or run `numba_cuda_mlir`. Dedicated `numbast-mlir` CI and real MLIR test coverage should be added in a follow-up PR. Current-CI-safe coverage added here: - Config parsing/routing test that monkeypatches MLIR dispatch without importing or running `numba_cuda_mlir`. - Guard test for `Module Link Variables Used` requiring `MLIR Backend: true`. - Schema-doc tests for the new config keys. ## Validation - `PYTHONPATH=numbast/src:ast_canopy python -m pytest numbast/src/numbast/tools/tests/test_mlir_backend_routing.py numbast/src/numbast/tools/tests/test_config_schema_docs.py` - `python -m py_compile ci/run_tests.py numbast/src/numbast/tools/static_binding_generator.py numbast/src/numbast/tools/tests/test_mlir_backend_routing.py numbast/src/numbast/tools/tests/test_config_schema_docs.py` - `PYTHONPATH=numbast/src:ast_canopy python -m pytest --collect-only numbast/ --ignore=numbast/src/numbast/experimental/mlir --ignore=numbast/src/numbast/tools/tests/test_symbol_exposure.py` The collect-only command needs the extra `test_symbol_exposure.py` ignore in this local environment because no CUDA device is available; the MLIR ignore itself prevented `numba_cuda_mlir` collection errors. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Adds an experimental MLIR backend for CUDA/C++ bindings (functions, function templates, class templates, enums, structs) with configurable argument-intent semantics and shim/link support. * Static binding generator now supports an MLIR backend mode and CLI-driven generation for MLIR workflows. * **Documentation** * Docs updated with MLIR backend requirements and usage notes. * **Tests / Chores** * Large suite of MLIR-focused tests added; CI/test configs updated to skip the experimental MLIR subtree by default. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/NVIDIA/numbast/pull/346?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Michael Wang <isVoid@users.noreply.github.com>
1 parent 90d9a90 commit fb74340

130 files changed

Lines changed: 15466 additions & 14 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/wheels-test.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ jobs:
136136
python -m pytest ast_canopy/tests
137137
- name: Test numbast wheels
138138
run: |
139-
python -m pytest numbast/
139+
python -m pytest \
140+
numbast/ \
141+
--ignore=numbast/src/numbast/experimental/mlir
140142
- name: Run numbast_extensions third-party CCCL tests
141143
run: |
142144
python -m pytest numbast_extensions/tests/thirdparty/CCCL/

ci/run_tests.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@
55
import pytest
66

77

8-
def run_pytest(lib, test_dir):
8+
MLIR_TESTS_DIR = "numbast/src/numbast/experimental/mlir"
9+
10+
11+
def run_pytest(lib, test_dir, extra_pytest_args=None):
912
if RAPIDS_TESTS_DIR := os.environ.get("RAPIDS_TESTS_DIR", None):
1013
junitxml = os.path.join(RAPIDS_TESTS_DIR, f"junit-{lib}.xml")
1114
else:
1215
junitxml = "/dev/null"
1316

17+
extra_pytest_args = extra_pytest_args or []
1418
command = [
1519
"pytest",
1620
"-v",
1721
"-s",
1822
"--continue-on-collection-errors",
1923
"--cache-clear",
2024
f"--junitxml={junitxml}",
25+
*extra_pytest_args,
2126
*test_dir,
2227
]
2328
try:
@@ -57,7 +62,7 @@ def run(
5762
if all_tests or ast_canopy:
5863
run_pytest("ast_canopy", ["ast_canopy/"])
5964
if all_tests or numbast:
60-
run_pytest("numbast", ["numbast/"])
65+
run_pytest("numbast", ["numbast/"], [f"--ignore={MLIR_TESTS_DIR}"])
6166
if all_tests or bf16:
6267
run_pytest(
6368
"bf16",

docs/source/static.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ drift between implementation and documentation.
2828
For details on ``Function Argument Intents`` semantics and generated signatures,
2929
see :doc:`/argument_intents`.
3030

31+
The experimental ``numbast-mlir`` backend is selected with
32+
``MLIR Backend: true`` and is imported from ``numbast.experimental.mlir``. It
33+
requires a user-provided ``numba_cuda_mlir`` installation; the default Numbast
34+
test environments do not install or run it yet.
35+
3136
Config example:
3237

3338
.. code-block:: yaml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Experimental Numbast namespaces."""
5+
6+
__all__ = []
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Numbast MLIR Backend
2+
3+
This package contains the experimental `numbast-mlir` backend under the
4+
`numbast.experimental.mlir` namespace.
5+
6+
It is not imported or re-exported by top-level `numbast`. Runtime use requires a
7+
user-provided `numba_cuda_mlir` installation, and the repository's default CI
8+
jobs intentionally do not collect or run these tests yet.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from numbast.experimental.mlir.struct import bind_cxx_struct, bind_cxx_structs
5+
from numbast.experimental.mlir.class_template import (
6+
bind_cxx_class_template_specialization,
7+
bind_cxx_class_template,
8+
bind_cxx_class_templates,
9+
clear_concrete_type_caches,
10+
)
11+
from numbast.experimental.mlir.function import (
12+
bind_cxx_function,
13+
bind_cxx_functions,
14+
)
15+
from numbast.experimental.mlir.function_template import (
16+
bind_cxx_function_template,
17+
bind_cxx_function_templates,
18+
)
19+
from numbast.experimental.mlir.enum import bind_cxx_enum, bind_cxx_enums
20+
from numbast.experimental.mlir.shim_writer import (
21+
MemoryShimWriter,
22+
FileShimWriter,
23+
)
24+
25+
import importlib.metadata
26+
27+
try:
28+
__version__ = importlib.metadata.version("numbast")
29+
except importlib.metadata.PackageNotFoundError:
30+
__version__ = "0+unknown"
31+
32+
__all__ = [
33+
"__version__",
34+
"bind_cxx_enum",
35+
"bind_cxx_enums",
36+
"bind_cxx_function",
37+
"bind_cxx_functions",
38+
"bind_cxx_function_template",
39+
"bind_cxx_function_templates",
40+
"bind_cxx_struct",
41+
"bind_cxx_structs",
42+
"bind_cxx_class_template_specialization",
43+
"bind_cxx_class_template",
44+
"bind_cxx_class_templates",
45+
"clear_concrete_type_caches",
46+
"MemoryShimWriter",
47+
"FileShimWriter",
48+
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from numba_cuda_mlir.mlir_lowering import MLIRLower
5+
6+
from numba_cuda_mlir._mlir import ir
7+
from numba_cuda_mlir._mlir.dialects import llvm
8+
9+
10+
def prepare_ir_types(
11+
builder: MLIRLower,
12+
argtys: list[ir.Type],
13+
*,
14+
pass_ptr_mask: list[bool] | None = None,
15+
) -> list[ir.Type]:
16+
"""
17+
Prepare LLVM IR types for passing function arguments by reference.
18+
19+
Given a list of argument IR types, return a parallel list of IR types suitable for an ABI that passes arguments by pointer. For each argument, the builder's get_mlir_type() is used to obtain the value type; if the corresponding entry in pass_ptr_mask is True and that value type is already an llvm.PointerType, that pointer type is preserved, otherwise the value type is wrapped in an opaque LLVM pointer type.
20+
21+
Parameters:
22+
builder (MLIRLower): Lowering helper used to obtain the value type via get_mlir_type().
23+
argtys (list[ir.Type]): Argument IR types to prepare.
24+
pass_ptr_mask (list[bool] | None): Optional mask the same length as argtys indicating per-argument behavior.
25+
If None, all entries are treated as False. When True for an argument and the value type is an llvm.PointerType,
26+
the pointer type is passed through unchanged.
27+
28+
Returns:
29+
list[ir.Type]: Prepared IR types where each entry is either a pointer-to-value or an existing pointer type preserved per pass_ptr_mask.
30+
31+
Raises:
32+
ValueError: If pass_ptr_mask is provided and its length does not match len(argtys).
33+
"""
34+
if pass_ptr_mask is None:
35+
pass_ptr_mask = [False] * len(argtys)
36+
37+
if len(pass_ptr_mask) != len(argtys):
38+
raise ValueError(
39+
f"pass_ptr_mask length ({len(pass_ptr_mask)}) must match argtys length ({len(argtys)})"
40+
)
41+
42+
ir_types: list[ir.Type] = []
43+
for argty, passthrough in zip(argtys, pass_ptr_mask):
44+
vty = builder.get_mlir_type(argty)
45+
if passthrough and isinstance(vty, llvm.PointerType):
46+
# Pass pointer-typed values directly (e.g. C++ T& mapped to CPointer(T))
47+
ir_types.append(vty)
48+
else:
49+
# Default ABI: pass pointer-to-value
50+
ir_types.append(ir.Type.parse("!llvm.ptr", context=vty.context))
51+
52+
return ir_types
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from numba_cuda_mlir import cuda
2+
from numba_cuda_mlir.types import float32
3+
import numpy as np
4+
5+
import pytest
6+
7+
8+
@pytest.mark.skip(reason="benchmark is not run by default")
9+
def test_arithmetic(benchmark):
10+
from numbast_extensions.bf16 import nv_bfloat16, get_shims # noqa: E402
11+
12+
def bench():
13+
@cuda.jit(link=get_shims())
14+
def kernel(arith):
15+
# Binary Arithmetic Operators
16+
a = nv_bfloat16(1.0)
17+
b = nv_bfloat16(2.0)
18+
19+
arith[0] = float32(a + b)
20+
arith[1] = float32(a - b)
21+
arith[2] = float32(a * b)
22+
arith[3] = float32(a / b)
23+
24+
arith = np.zeros(4, dtype=np.float32)
25+
26+
kernel[1, 1](arith)
27+
28+
benchmark(bench)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from tempfile import NamedTemporaryFile
5+
6+
import pytest
7+
8+
from numba_cuda_mlir import cuda
9+
10+
from ast_canopy import parse_declarations_from_source
11+
from numbast.experimental.mlir import bind_cxx_functions, MemoryShimWriter
12+
13+
function_template = """
14+
__device__ int {name}(int a, int b) {{
15+
return a + b;
16+
}}
17+
"""
18+
19+
20+
@pytest.fixture(params=[1, 10, 100, 1000])
21+
def simulate_header(request):
22+
"""Create a header file that contains N headers to add two integers together.
23+
Used to benchmark the impact of number of headers to kernel launch.
24+
"""
25+
N = request.param
26+
27+
tmp = NamedTemporaryFile(mode="w", suffix=".cuh", delete=False)
28+
functions = [function_template.format(name=f"add{i}") for i in range(N)]
29+
tmp.write("\n".join(functions))
30+
tmp.flush()
31+
32+
major, minor = cuda.get_current_device().compute_capability
33+
decls = parse_declarations_from_source(
34+
tmp.name, [tmp.name], f"sm_{major}{minor}"
35+
)
36+
shim_writer = MemoryShimWriter(f'#include "{tmp.name}"')
37+
adds = bind_cxx_functions(shim_writer, decls.functions)
38+
39+
yield adds, shim_writer
40+
41+
42+
@pytest.mark.skip(reason="benchmark is not run by default")
43+
def test_rtc(benchmark, simulate_header):
44+
def bench():
45+
adds, shim_writer = simulate_header
46+
add = adds[0]
47+
48+
@cuda.jit(link=shim_writer.links())
49+
def kernel():
50+
_ = add(1, 2)
51+
52+
kernel[1, 1]()
53+
54+
benchmark(bench)

0 commit comments

Comments
 (0)