blob: bc038d09d560650dbd4641d042d12c8e5c87de36 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Test CUBIN launcher functionality using load_inline."""
from __future__ import annotations
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
try:
import torch
import torch.version
except ImportError:
torch = None # ty: ignore[invalid-assignment]
import tvm_ffi.cpp
from tvm_ffi.testing import run_with_gpu_lock
# Check if CUDA is available
def _is_cuda_available() -> bool:
"""Check if CUDA is available for testing."""
if torch is None:
return False
return torch.cuda.is_available()
def _is_cuda_version_greater_than_13() -> bool:
"""Check if CUDA version is greater than 13.0."""
if torch is None or not torch.cuda.is_available():
return False
if torch.version.cuda is None:
return False
try:
# Parse version string into tuple of integers (e.g., "12.1" -> (12, 1))
version_parts = tuple(int(x) for x in torch.version.cuda.split("."))
return version_parts > (13, 0)
except (ValueError, TypeError, AttributeError):
return False
def _compile_kernel_to_cubin() -> bytes:
"""Compile simple CUDA kernels to CUBIN.
Returns the raw CUBIN bytes.
"""
cuda_code = r"""
extern "C" __global__ void add_one_cuda(const float* x, float* y, int64_t n) {
int64_t idx = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
if (idx < n) {
y[idx] = x[idx] + 1.0f;
}
}
extern "C" __global__ void mul_two_cuda(const float* x, float* y, int64_t n) {
int64_t idx = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
if (idx < n) {
y[idx] = x[idx] * 2.0f;
}
}
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
cu_file = tmppath / "kernels.cu"
cubin_file = tmppath / "kernels.cubin"
cu_file.write_text(cuda_code)
# Compile to CUBIN using nvcc
result = subprocess.run(
["nvcc", "--cubin", "-arch=native", str(cu_file), "-o", str(cubin_file)],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
pytest.skip(f"nvcc not available or compilation failed: {result.stderr}") # ty: ignore[invalid-argument-type, too-many-positional-arguments]
return cubin_file.read_bytes()
@pytest.mark.skipif(sys.platform != "linux", reason="CUBIN launcher only supported on Linux")
@pytest.mark.skipif(torch is None, reason="PyTorch not installed")
@pytest.mark.skipif(not _is_cuda_available(), reason="CUDA not available")
@pytest.mark.skipif(
not _is_cuda_version_greater_than_13(), reason="CUDA version must be greater than 13.0"
)
def test_cubin_launcher_add_one() -> None:
"""Test loading and launching add_one kernel from CUBIN."""
assert torch is not None, "PyTorch is required for this test"
cubin_bytes = _compile_kernel_to_cubin()
# Define C++ code to load and launch the CUBIN kernel
cpp_code = """
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/extra/c_env_api.h>
#include <tvm/ffi/extra/cuda/cubin_launcher.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/string.h>
#include <cstring>
#include <memory>
namespace cubin_test {
static std::unique_ptr<tvm::ffi::CubinModule> g_module;
static std::unique_ptr<tvm::ffi::CubinKernel> g_kernel_add_one;
static std::unique_ptr<tvm::ffi::CubinKernel> g_kernel_mul_two;
void LoadCubinData(const tvm::ffi::Bytes& cubin_data) {
// Load CUBIN from bytes
g_module = std::make_unique<tvm::ffi::CubinModule>(cubin_data);
g_kernel_add_one = std::make_unique<tvm::ffi::CubinKernel>((*g_module)["add_one_cuda"]);
g_kernel_mul_two = std::make_unique<tvm::ffi::CubinKernel>((*g_module)["mul_two_cuda"]);
}
void LaunchAddOne(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
TVM_FFI_CHECK(g_module != nullptr, RuntimeError) << "CUBIN module not loaded";
TVM_FFI_CHECK(x.ndim() == 1, ValueError) << "Input must be 1D tensor";
TVM_FFI_CHECK(y.ndim() == 1, ValueError) << "Output must be 1D tensor";
TVM_FFI_CHECK(x.size(0) == y.size(0), ValueError) << "Sizes must match";
int64_t n = x.size(0);
void* x_ptr = x.data_ptr();
void* y_ptr = y.data_ptr();
void* args[] = {
reinterpret_cast<void*>(&x_ptr),
reinterpret_cast<void*>(&y_ptr),
reinterpret_cast<void*>(&n),
};
tvm::ffi::dim3 grid((n + 1023) / 1024);
tvm::ffi::dim3 block(1024);
DLDevice device = x.device();
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(device.device_type, device.device_id));
auto result = g_kernel_add_one->Launch(args, grid, block, stream);
TVM_FFI_CHECK_CUBIN_LAUNCHER_CUDA_ERROR(result);
}
void LaunchMulTwo(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
TVM_FFI_CHECK(g_module != nullptr, RuntimeError) << "CUBIN module not loaded";
TVM_FFI_CHECK(x.ndim() == 1, ValueError) << "Input must be 1D tensor";
TVM_FFI_CHECK(y.ndim() == 1, ValueError) << "Output must be 1D tensor";
TVM_FFI_CHECK(x.size(0) == y.size(0), ValueError) << "Sizes must match";
int64_t n = x.size(0);
void* x_ptr = x.data_ptr();
void* y_ptr = y.data_ptr();
void* args[] = {
reinterpret_cast<void*>(&x_ptr),
reinterpret_cast<void*>(&y_ptr),
reinterpret_cast<void*>(&n),
};
tvm::ffi::dim3 grid((n + 1023) / 1024);
tvm::ffi::dim3 block(1024);
DLDevice device = x.device();
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(device.device_type, device.device_id));
auto result = g_kernel_mul_two->Launch(args, grid, block, stream);
TVM_FFI_CHECK_CUBIN_LAUNCHER_CUDA_ERROR(result);
}
TVM_FFI_DLL_EXPORT_TYPED_FUNC(load_cubin_data, cubin_test::LoadCubinData);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(launch_add_one, cubin_test::LaunchAddOne);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(launch_mul_two, cubin_test::LaunchMulTwo);
} // namespace cubin_test
"""
# Compile and load the C++ code
mod = tvm_ffi.cpp.load_inline(
"cubin_test",
cuda_sources=cpp_code,
extra_ldflags=["-lcudart"],
)
def run_and_check() -> None:
assert torch is not None
# Load CUBIN from bytes
load_fn = mod["load_cubin_data"]
load_fn(cubin_bytes)
# Test add_one kernel
launch_add_one = mod["launch_add_one"]
n = 256
x = torch.arange(n, dtype=torch.float32, device="cuda")
y = torch.empty(n, dtype=torch.float32, device="cuda")
launch_add_one(x, y)
expected = x + 1
torch.testing.assert_close(y, expected)
# Test mul_two kernel
launch_mul_two = mod["launch_mul_two"]
x = torch.arange(n, dtype=torch.float32, device="cuda") * 0.5
y = torch.empty(n, dtype=torch.float32, device="cuda")
launch_mul_two(x, y)
expected = x * 2
torch.testing.assert_close(y, expected)
run_with_gpu_lock(run_and_check)
@pytest.mark.skipif(sys.platform != "linux", reason="CUBIN launcher only supported on Linux")
@pytest.mark.skipif(torch is None, reason="PyTorch not installed")
@pytest.mark.skipif(not _is_cuda_available(), reason="CUDA not available")
@pytest.mark.skipif(
not _is_cuda_version_greater_than_13(), reason="CUDA version must be greater than 13.0"
)
def test_cubin_launcher_launch_ex() -> None:
"""Test LaunchEx with ConstructLaunchConfig (no clustering)."""
assert torch is not None, "PyTorch is required for this test"
cubin_bytes = _compile_kernel_to_cubin()
cpp_code = """
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/extra/c_env_api.h>
#include <tvm/ffi/extra/cuda/cubin_launcher.h>
#include <tvm/ffi/function.h>
#include <memory>
namespace cubin_test_launch_ex {
static std::unique_ptr<tvm::ffi::CubinModule> g_module;
static std::unique_ptr<tvm::ffi::CubinKernel> g_kernel_add_one;
void LoadCubinData(const tvm::ffi::Bytes& cubin_data) {
g_module = std::make_unique<tvm::ffi::CubinModule>(cubin_data);
g_kernel_add_one = std::make_unique<tvm::ffi::CubinKernel>((*g_module)["add_one_cuda"]);
}
void LaunchAddOneEx(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
TVM_FFI_CHECK(g_module != nullptr, RuntimeError) << "CUBIN module not loaded";
TVM_FFI_CHECK(x.ndim() == 1, ValueError) << "Input must be 1D tensor";
TVM_FFI_CHECK(y.ndim() == 1, ValueError) << "Output must be 1D tensor";
TVM_FFI_CHECK(x.size(0) == y.size(0), ValueError) << "Sizes must match";
int64_t n = x.size(0);
void* x_ptr = x.data_ptr();
void* y_ptr = y.data_ptr();
void* args[] = {&x_ptr, &y_ptr, &n};
tvm::ffi::dim3 grid((n + 1023) / 1024);
tvm::ffi::dim3 block(1024);
DLDevice device = x.device();
auto stream = static_cast<tvm::ffi::cuda_api::StreamHandle>(
TVMFFIEnvGetStream(device.device_type, device.device_id));
// Use ConstructLaunchConfig + LaunchEx (cluster_dim=1 means no clustering)
tvm::ffi::cuda_api::LaunchConfig config;
tvm::ffi::cuda_api::LaunchAttrType attr;
auto err = tvm::ffi::cuda_api::ConstructLaunchConfig(
g_kernel_add_one->GetHandle(), stream, /*smem_size=*/0,
grid, block, /*cluster_dim=*/1, config, attr);
TVM_FFI_CHECK_CUBIN_LAUNCHER_CUDA_ERROR(err);
auto result = g_kernel_add_one->LaunchEx(args, config);
TVM_FFI_CHECK_CUBIN_LAUNCHER_CUDA_ERROR(result);
}
TVM_FFI_DLL_EXPORT_TYPED_FUNC(load_cubin_data, cubin_test_launch_ex::LoadCubinData);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(launch_add_one_ex, cubin_test_launch_ex::LaunchAddOneEx);
} // namespace cubin_test_launch_ex
"""
mod = tvm_ffi.cpp.load_inline(
"cubin_test_launch_ex",
cuda_sources=cpp_code,
extra_ldflags=["-lcudart"],
)
def run_and_check() -> None:
assert torch is not None
load_fn = mod["load_cubin_data"]
load_fn(cubin_bytes)
launch_add_one_ex = mod["launch_add_one_ex"]
n = 256
x = torch.arange(n, dtype=torch.float32, device="cuda")
y = torch.empty(n, dtype=torch.float32, device="cuda")
launch_add_one_ex(x, y)
expected = x + 1
torch.testing.assert_close(y, expected)
run_with_gpu_lock(run_and_check)
@pytest.mark.skipif(sys.platform != "linux", reason="CUBIN launcher only supported on Linux")
@pytest.mark.skipif(torch is None, reason="PyTorch not installed")
@pytest.mark.skipif(not _is_cuda_available(), reason="CUDA not available")
@pytest.mark.skipif(
not _is_cuda_version_greater_than_13(), reason="CUDA version must be greater than 13.0"
)
def test_cubin_launcher_chained() -> None:
"""Test chaining multiple kernel launches."""
assert torch is not None, "PyTorch is required for this test"
cubin_bytes = _compile_kernel_to_cubin()
cpp_code = """
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/extra/c_env_api.h>
#include <tvm/ffi/extra/cuda/cubin_launcher.h>
#include <tvm/ffi/function.h>
#include <memory>
namespace cubin_test_chain {
static std::unique_ptr<tvm::ffi::CubinModule> g_module;
static std::unique_ptr<tvm::ffi::CubinKernel> g_kernel_add_one;
static std::unique_ptr<tvm::ffi::CubinKernel> g_kernel_mul_two;
void LoadCubinData(const tvm::ffi::Bytes& cubin_data) {
// Load CUBIN from bytes
g_module = std::make_unique<tvm::ffi::CubinModule>(cubin_data);
g_kernel_add_one = std::make_unique<tvm::ffi::CubinKernel>((*g_module)["add_one_cuda"]);
g_kernel_mul_two = std::make_unique<tvm::ffi::CubinKernel>((*g_module)["mul_two_cuda"]);
}
void LaunchAddOne(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
TVM_FFI_CHECK(g_module != nullptr, RuntimeError) << "CUBIN module not loaded";
TVM_FFI_CHECK(x.ndim() == 1, ValueError) << "Input must be 1D tensor";
int64_t n = x.size(0);
void* x_ptr = x.data_ptr();
void* y_ptr = y.data_ptr();
void* args[] = {&x_ptr, &y_ptr, &n};
tvm::ffi::dim3 grid((n + 1023) / 1024);
tvm::ffi::dim3 block(1024);
DLDevice device = x.device();
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(device.device_type, device.device_id));
g_kernel_add_one->Launch(args, grid, block, stream);
}
void LaunchMulTwo(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
TVM_FFI_CHECK(g_module != nullptr, RuntimeError) << "CUBIN module not loaded";
int64_t n = x.size(0);
void* x_ptr = x.data_ptr();
void* y_ptr = y.data_ptr();
void* args[] = {&x_ptr, &y_ptr, &n};
tvm::ffi::dim3 grid((n + 1023) / 1024);
tvm::ffi::dim3 block(1024);
DLDevice device = x.device();
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(device.device_type, device.device_id));
g_kernel_mul_two->Launch(args, grid, block, stream);
}
TVM_FFI_DLL_EXPORT_TYPED_FUNC(load_cubin_data, cubin_test_chain::LoadCubinData);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(launch_add_one, cubin_test_chain::LaunchAddOne);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(launch_mul_two, cubin_test_chain::LaunchMulTwo);
} // namespace cubin_test_chain
"""
mod = tvm_ffi.cpp.load_inline("cubin_test_chain", cuda_sources=cpp_code)
def run_and_check() -> None:
assert torch is not None
# Load CUBIN from bytes
load_fn = mod["load_cubin_data"]
load_fn(cubin_bytes)
launch_add_one = mod["launch_add_one"]
launch_mul_two = mod["launch_mul_two"]
# Test chained execution: (x + 1) * 2
n = 128
x = torch.full((n,), 5.0, dtype=torch.float32, device="cuda")
temp = torch.empty(n, dtype=torch.float32, device="cuda")
y = torch.empty(n, dtype=torch.float32, device="cuda")
launch_add_one(x, temp) # temp = x + 1 = 6
launch_mul_two(temp, y) # y = temp * 2 = 12
expected = torch.full((n,), 12.0, dtype=torch.float32, device="cuda")
torch.testing.assert_close(y, expected)
run_with_gpu_lock(run_and_check)