blob: 0938a251de80d6a8a026736e9ee8107ec8482a52 [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.
from __future__ import annotations
import ctypes
import sys
import pytest
try:
import torch
# Import tvm_ffi to load the DLPack exchange API extension
# This sets torch.Tensor.__dlpack_c_exchange_api__
import tvm_ffi
from torch.utils import cpp_extension
from tvm_ffi import libinfo
except ImportError:
torch = None # ty: ignore[invalid-assignment]
# Check if DLPack Exchange API is available
_has_dlpack_api = torch is not None and hasattr(torch.Tensor, "__dlpack_c_exchange_api__")
_has_gpu = torch is not None and torch.cuda.is_available()
@pytest.mark.skipif(not _has_dlpack_api, reason="PyTorch DLPack Exchange API not available")
def test_dlpack_exchange_api() -> None:
# xfail the test on windows platform, it seems to be a bug in torch extension building on windows
if sys.platform.startswith("win"):
pytest.xfail("DLPack Exchange API test is known to fail on Windows platform")
assert torch is not None
assert hasattr(torch.Tensor, "__dlpack_c_exchange_api__")
api_attr = torch.Tensor.__dlpack_c_exchange_api__
# PyCapsule - extract the pointer as integer
pythonapi = ctypes.pythonapi
# Set restype to c_size_t to get integer directly (avoids c_void_p quirks)
pythonapi.PyCapsule_GetPointer.restype = ctypes.c_size_t
pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
capsule_name = b"dlpack_exchange_api"
api_ptr = pythonapi.PyCapsule_GetPointer(api_attr, capsule_name)
assert api_ptr != 0, "API pointer from PyCapsule should not be NULL"
tensor = torch.arange(24, dtype=torch.float32).reshape(2, 3, 4)
source = """
#include <torch/extension.h>
#include <dlpack/dlpack.h>
#include <memory>
void test_dlpack_api(at::Tensor tensor, int64_t api_ptr_int, bool cuda_available) {
DLPackExchangeAPI* api = reinterpret_cast<DLPackExchangeAPI*>(api_ptr_int);
// Test 1: API structure and version
{
TORCH_CHECK(api != nullptr, "API pointer is NULL");
TORCH_CHECK(api->header.version.major == DLPACK_MAJOR_VERSION,
"Expected major version ", DLPACK_MAJOR_VERSION, ", got ", api->header.version.major);
TORCH_CHECK(api->header.version.minor == DLPACK_MINOR_VERSION,
"Expected minor version ", DLPACK_MINOR_VERSION, ", got ", api->header.version.minor);
TORCH_CHECK(api->managed_tensor_allocator != nullptr,
"managed_tensor_allocator is NULL");
TORCH_CHECK(api->managed_tensor_from_py_object_no_sync != nullptr,
"managed_tensor_from_py_object_no_sync is NULL");
TORCH_CHECK(api->managed_tensor_to_py_object_no_sync != nullptr,
"managed_tensor_to_py_object_no_sync is NULL");
TORCH_CHECK(api->dltensor_from_py_object_no_sync != nullptr,
"dltensor_from_py_object_no_sync is NULL");
TORCH_CHECK(api->current_work_stream != nullptr,
"current_work_stream is NULL");
}
// Test 2: managed_tensor_allocator
{
DLTensor prototype;
prototype.device.device_type = kDLCPU;
prototype.device.device_id = 0;
prototype.ndim = 3;
int64_t shape[3] = {3, 4, 5};
prototype.shape = shape;
prototype.strides = nullptr;
DLDataType dtype;
dtype.code = kDLFloat;
dtype.bits = 32;
dtype.lanes = 1;
prototype.dtype = dtype;
prototype.data = nullptr;
prototype.byte_offset = 0;
DLManagedTensorVersioned* out_tensor = nullptr;
int result = api->managed_tensor_allocator(&prototype, &out_tensor, nullptr, nullptr);
TORCH_CHECK(result == 0, "Allocator failed with code ", result);
TORCH_CHECK(out_tensor != nullptr, "Allocator returned NULL");
TORCH_CHECK(out_tensor->dl_tensor.ndim == 3, "Expected ndim 3, got ", out_tensor->dl_tensor.ndim);
TORCH_CHECK(out_tensor->dl_tensor.shape[0] == 3, "Expected shape[0] = 3, got ", out_tensor->dl_tensor.shape[0]);
TORCH_CHECK(out_tensor->dl_tensor.shape[1] == 4, "Expected shape[1] = 4, got ", out_tensor->dl_tensor.shape[1]);
TORCH_CHECK(out_tensor->dl_tensor.shape[2] == 5, "Expected shape[2] = 5, got ", out_tensor->dl_tensor.shape[2]);
TORCH_CHECK(out_tensor->dl_tensor.dtype.code == kDLFloat, "Expected dtype code kDLFloat, got ", out_tensor->dl_tensor.dtype.code);
TORCH_CHECK(out_tensor->dl_tensor.dtype.bits == 32, "Expected dtype bits 32, got ", out_tensor->dl_tensor.dtype.bits);
TORCH_CHECK(out_tensor->dl_tensor.device.device_type == kDLCPU, "Expected device type kDLCPU, got ", out_tensor->dl_tensor.device.device_type);
if (out_tensor->deleter) {
out_tensor->deleter(out_tensor);
}
}
// Test 3: managed_tensor_from_py_object_no_sync
{
std::unique_ptr<PyObject, decltype(&Py_DecRef)> py_obj(THPVariable_Wrap(tensor), &Py_DecRef);
TORCH_CHECK(py_obj.get() != nullptr, "Failed to wrap tensor to PyObject");
DLManagedTensorVersioned* out_tensor = nullptr;
int result = api->managed_tensor_from_py_object_no_sync(py_obj.get(), &out_tensor);
TORCH_CHECK(result == 0, "from_py_object_no_sync failed with code ", result);
TORCH_CHECK(out_tensor != nullptr, "from_py_object_no_sync returned NULL");
TORCH_CHECK(out_tensor->version.major == DLPACK_MAJOR_VERSION,
"Expected major version ", DLPACK_MAJOR_VERSION, ", got ", out_tensor->version.major);
TORCH_CHECK(out_tensor->version.minor == DLPACK_MINOR_VERSION,
"Expected minor version ", DLPACK_MINOR_VERSION, ", got ", out_tensor->version.minor);
TORCH_CHECK(out_tensor->dl_tensor.ndim == 3, "Expected ndim 3, got ", out_tensor->dl_tensor.ndim);
TORCH_CHECK(out_tensor->dl_tensor.shape[0] == 2, "Expected shape[0] = 2, got ", out_tensor->dl_tensor.shape[0]);
TORCH_CHECK(out_tensor->dl_tensor.shape[1] == 3, "Expected shape[1] = 3, got ", out_tensor->dl_tensor.shape[1]);
TORCH_CHECK(out_tensor->dl_tensor.shape[2] == 4, "Expected shape[2] = 4, got ", out_tensor->dl_tensor.shape[2]);
TORCH_CHECK(out_tensor->dl_tensor.dtype.code == kDLFloat, "Expected dtype code kDLFloat, got ", out_tensor->dl_tensor.dtype.code);
TORCH_CHECK(out_tensor->dl_tensor.dtype.bits == 32, "Expected dtype bits 32, got ", out_tensor->dl_tensor.dtype.bits);
TORCH_CHECK(out_tensor->dl_tensor.data != nullptr, "Data pointer is NULL");
if (out_tensor->deleter) {
out_tensor->deleter(out_tensor);
}
}
// Test 4: managed_tensor_to_py_object_no_sync
{
std::unique_ptr<PyObject, decltype(&Py_DecRef)> py_obj(THPVariable_Wrap(tensor), &Py_DecRef);
TORCH_CHECK(py_obj.get() != nullptr, "Failed to wrap tensor to PyObject");
DLManagedTensorVersioned* managed_tensor = nullptr;
int result = api->managed_tensor_from_py_object_no_sync(py_obj.get(), &managed_tensor);
TORCH_CHECK(result == 0, "from_py_object_no_sync failed");
TORCH_CHECK(managed_tensor != nullptr, "from_py_object_no_sync returned NULL");
std::unique_ptr<PyObject, decltype(&Py_DecRef)> py_obj_out(nullptr, &Py_DecRef);
PyObject* py_obj_out_raw = nullptr;
result = api->managed_tensor_to_py_object_no_sync(managed_tensor, reinterpret_cast<void**>(&py_obj_out_raw));
py_obj_out.reset(py_obj_out_raw);
TORCH_CHECK(result == 0, "to_py_object_no_sync failed with code ", result);
TORCH_CHECK(py_obj_out.get() != nullptr, "to_py_object_no_sync returned NULL");
TORCH_CHECK(THPVariable_Check(py_obj_out.get()), "Returned PyObject is not a Tensor");
at::Tensor result_tensor = THPVariable_Unpack(py_obj_out.get());
TORCH_CHECK(result_tensor.dim() == 3, "Expected 3 dimensions, got ", result_tensor.dim());
TORCH_CHECK(result_tensor.size(0) == 2, "Expected size(0) = 2, got ", result_tensor.size(0));
TORCH_CHECK(result_tensor.size(1) == 3, "Expected size(1) = 3, got ", result_tensor.size(1));
TORCH_CHECK(result_tensor.size(2) == 4, "Expected size(2) = 4, got ", result_tensor.size(2));
TORCH_CHECK(result_tensor.scalar_type() == at::kFloat, "Expected dtype kFloat, got ", result_tensor.scalar_type());
}
// Test 5: dltensor_from_py_object_no_sync
{
std::unique_ptr<PyObject, decltype(&Py_DecRef)> py_obj(THPVariable_Wrap(tensor), &Py_DecRef);
TORCH_CHECK(py_obj.get() != nullptr, "Failed to wrap tensor to PyObject");
DLTensor dltensor;
int result = api->dltensor_from_py_object_no_sync(py_obj.get(), &dltensor);
TORCH_CHECK(result == 0, "dltensor_from_py_object_no_sync failed with code ", result);
TORCH_CHECK(dltensor.ndim == 3, "Expected ndim 3, got ", dltensor.ndim);
TORCH_CHECK(dltensor.shape[0] == 2, "Expected shape[0] = 2, got ", dltensor.shape[0]);
TORCH_CHECK(dltensor.shape[1] == 3, "Expected shape[1] = 3, got ", dltensor.shape[1]);
TORCH_CHECK(dltensor.shape[2] == 4, "Expected shape[2] = 4, got ", dltensor.shape[2]);
TORCH_CHECK(dltensor.dtype.code == kDLFloat, "Expected dtype code kDLFloat, got ", dltensor.dtype.code);
TORCH_CHECK(dltensor.dtype.bits == 32, "Expected dtype bits 32, got ", dltensor.dtype.bits);
TORCH_CHECK(dltensor.data != nullptr, "Data pointer is NULL");
}
// Test 6: current_work_stream (CUDA if available, otherwise CPU)
{
void* stream_out = nullptr;
DLDeviceType device_type = cuda_available ? kDLCUDA : kDLCPU;
int result = api->current_work_stream(device_type, 0, &stream_out);
TORCH_CHECK(result == 0, "current_work_stream failed with code ", result);
}
}
"""
include_paths = libinfo.include_paths()
if torch.cuda.is_available():
include_paths += cpp_extension.include_paths("cuda")
mod = cpp_extension.load_inline(
name="dlpack_test",
cpp_sources=[source],
functions=["test_dlpack_api"],
extra_include_paths=include_paths,
)
# Run the comprehensive test
mod.test_dlpack_api(tensor, api_ptr, torch.cuda.is_available())
@pytest.mark.skipif(
not (_has_dlpack_api and _has_gpu),
reason="PyTorch DLPack Exchange API with GPU is not available",
)
def test_dlpack_exchange_api_gpu_tensor_metadata() -> None:
assert torch is not None
echo = tvm_ffi.get_global_func("testing.echo")
for shape in [(512,), (512, 512), (2, 3, 4)]:
source = torch.empty(shape, device="cuda", dtype=torch.float16)
tvm_tensor = tvm_ffi.from_dlpack(source)
assert tvm_tensor.shape == shape
assert tvm_tensor.dtype == tvm_ffi.dtype("float16")
echoed = echo(source)
assert tuple(echoed.shape) == shape
assert echoed.dtype == source.dtype
assert echoed.device == source.device
@pytest.mark.skipif(not _has_dlpack_api, reason="PyTorch DLPack Exchange API not available")
def test_from_dlpack_torch() -> None:
# Covers from_dlpack to use fallback fastpath
assert torch is not None
tensor = torch.arange(24, dtype=torch.float32).reshape(2, 3, 4)
tensor_from_dlpack = tvm_ffi.from_dlpack(tensor)
assert tensor_from_dlpack.shape == tensor.shape
assert tensor_from_dlpack.dtype == tvm_ffi.float32
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])