blob: 7954ba007c6abe6eb559b9b8a502ca9fd41774f6 [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.
"""Tests for the embed_cubin utility."""
from __future__ import annotations
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
from tvm_ffi.utils.embed_cubin import embed_cubin
try:
import torch
except ImportError:
torch = None # ty: ignore[invalid-assignment]
def _is_cuda_available() -> bool:
"""Check if CUDA is available for testing."""
if torch is None:
return False
return torch.cuda.is_available()
def _create_test_object_file(obj_path: Path) -> None:
"""Create a simple test object file with TVM_FFI_EMBED_CUBIN macro usage.
We avoid including the full header to prevent CUDA dependency in tests.
Instead, we manually declare the external symbols that TVM_FFI_EMBED_CUBIN would create.
"""
cpp_code = """
// Manually declare the external symbols that will be provided by embed_cubin
// This is what TVM_FFI_EMBED_CUBIN(test_cubin) would do, but without CUDA headers
extern "C" {
extern const unsigned char __tvm_ffi__cubin_test_cubin[];
extern const unsigned char __tvm_ffi__cubin_test_cubin_end[];
}
// Simple function that references the embedded CUBIN symbols
const void* get_cubin_start() {
return __tvm_ffi__cubin_test_cubin;
}
const void* get_cubin_end() {
return __tvm_ffi__cubin_test_cubin_end;
}
"""
# Write C++ code to a temporary file
cpp_file = obj_path.with_suffix(".cc")
cpp_file.write_text(cpp_code)
# Compile to object file
# Try to find a C++ compiler
compilers = ["g++", "clang++", "c++"]
compiler = None
for c in compilers:
try:
subprocess.run(
[c, "--version"],
check=True,
capture_output=True,
)
compiler = c
break
except (subprocess.CalledProcessError, FileNotFoundError):
continue
if compiler is None:
pytest.skip("No C++ compiler found (tried g++, clang++, c++)") # ty: ignore[invalid-argument-type, too-many-positional-arguments]
assert isinstance(compiler, str), "Compiler is not a string"
compile_cmd: list[str] = [compiler, "-c", "-fPIC", "-o", str(obj_path), str(cpp_file)]
try:
subprocess.run(compile_cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
pytest.skip(f"Failed to compile test object file: {e.stderr.decode('utf-8')}") # ty: ignore[invalid-argument-type, too-many-positional-arguments]
finally:
# Clean up temporary C++ file
cpp_file.unlink(missing_ok=True)
def _create_test_cubin(cubin_path: Path) -> None:
"""Create a dummy CUBIN file for testing."""
# Create a simple binary file with some recognizable content
cubin_path.write_bytes(b"DUMMY_CUBIN_DATA_FOR_TESTING" * 10)
def _check_symbols_in_object(obj_path: Path, expected_symbols: list[str]) -> bool:
"""Check if expected symbols exist in the object file using nm."""
try:
result = subprocess.run(
["nm", str(obj_path)],
check=True,
capture_output=True,
text=True,
)
nm_output = result.stdout
for symbol in expected_symbols:
if symbol not in nm_output:
return False
return True
except (subprocess.CalledProcessError, FileNotFoundError):
pytest.skip("nm tool not available") # ty: ignore[invalid-argument-type, too-many-positional-arguments]
return False
@pytest.mark.skipif(sys.platform != "linux", reason="embed_cubin 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")
def test_embed_cubin_basic() -> None:
"""Test basic embed_cubin functionality."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test files
input_obj = temp_path / "input.o"
output_obj = temp_path / "output.o"
cubin_file = temp_path / "test.cubin"
_create_test_object_file(input_obj)
_create_test_cubin(cubin_file)
# Embed CUBIN into object file
embed_cubin(
cubin_path=cubin_file,
input_obj_path=input_obj,
output_obj_path=output_obj,
name="test_cubin",
verbose=False,
)
# Check that output file exists
assert output_obj.exists(), "Output object file was not created"
assert output_obj.stat().st_size > 0, "Output object file is empty"
# Check that the output is larger than the input (it should contain both)
assert output_obj.stat().st_size > input_obj.stat().st_size, (
"Output object file should be larger than input"
)
# Check that expected symbols are present
expected_symbols = [
"__tvm_ffi__cubin_test_cubin",
"__tvm_ffi__cubin_test_cubin_end",
]
assert _check_symbols_in_object(output_obj, expected_symbols), (
f"Expected symbols {expected_symbols} not found in output object file"
)
@pytest.mark.skipif(sys.platform != "linux", reason="embed_cubin 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")
def test_embed_cubin_different_names() -> None:
"""Test embedding CUBIN with different names."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test files
input_obj = temp_path / "input.o"
output_obj = temp_path / "output.o"
cubin_file = temp_path / "test.cubin"
_create_test_object_file(input_obj)
_create_test_cubin(cubin_file)
# Test with a different name
custom_name = "my_custom_kernel"
embed_cubin(
cubin_path=cubin_file,
input_obj_path=input_obj,
output_obj_path=output_obj,
name=custom_name,
verbose=False,
)
# Check that symbols with custom name are present
expected_symbols = [
f"__tvm_ffi__cubin_{custom_name}",
f"__tvm_ffi__cubin_{custom_name}_end",
]
assert _check_symbols_in_object(output_obj, expected_symbols), (
f"Expected symbols {expected_symbols} not found in output object file"
)
def test_embed_cubin_nonexistent_input() -> None:
"""Test that embed_cubin raises error for nonexistent input files."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
input_obj = temp_path / "nonexistent_input.o"
output_obj = temp_path / "output.o"
cubin_file = temp_path / "nonexistent.cubin"
# Test with nonexistent CUBIN file
with pytest.raises(FileNotFoundError):
embed_cubin(
cubin_path=cubin_file,
input_obj_path=input_obj,
output_obj_path=output_obj,
name="test",
verbose=False,
)
@pytest.mark.skipif(sys.platform != "linux", reason="embed_cubin 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")
def test_embed_cubin_verbose_mode() -> None:
"""Test that verbose mode doesn't crash."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test files
input_obj = temp_path / "input.o"
output_obj = temp_path / "output.o"
cubin_file = temp_path / "test.cubin"
_create_test_object_file(input_obj)
_create_test_cubin(cubin_file)
# Run with verbose mode (should not crash)
embed_cubin(
cubin_path=cubin_file,
input_obj_path=input_obj,
output_obj_path=output_obj,
name="test_cubin",
verbose=True, # Test verbose mode
)
assert output_obj.exists(), "Output object file was not created in verbose mode"