Skip to content

Commit edb38fb

Browse files
authored
Unit tests for creating and destroying tensors on GPU (#546)
* Misc updates to CUDA CI workflow * Add construction and destruction unit tests on CUDA device * Make CPU device index warning clearer * Account for unit test subdirectories in CUDA CI triggers
1 parent 952e999 commit edb38fb

4 files changed

Lines changed: 250 additions & 5 deletions

File tree

.github/workflows/test_suite_ubuntu_cuda_gnu.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ on:
1818
- 'src/*.cpp'
1919
- 'src/*.h'
2020
# Unit tests
21-
- 'test/unit/*.pf'
21+
- 'test/unit/**/*.pf'
2222
# Integration tests
2323
- 'examples/**/*.py'
2424
- 'examples/**/*.f90'
@@ -28,7 +28,7 @@ on:
2828

2929
pull_request:
3030
paths:
31-
- '.github/workflows/test_suite_ubuntu_cuda.yml'
31+
- '.github/workflows/test_suite_ubuntu_cuda_gnu.yml'
3232

3333
# Allows you to run this workflow manually from the Actions tab
3434
workflow_dispatch:
@@ -81,7 +81,7 @@ jobs:
8181
sudo apt update
8282
sudo apt install -y cmake nvidia-cuda-toolkit
8383
84-
# Currently used by example7_mpi
84+
# Currently used by example_mpi
8585
- name: Install an MPI distribution
8686
run: |
8787
sudo apt install -y openmpi-bin openmpi-common libopenmpi-dev
@@ -128,7 +128,7 @@ jobs:
128128
run: |
129129
. ftorch/bin/activate
130130
cd build
131-
ctest --verbose --tests-regex unit
131+
ctest --verbose --tests-regex cuda
132132
133133
- name: Run integration tests
134134
run: |

src/ctorch.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const auto get_libtorch_device(torch_device_t device_type, int device_index) {
9595
switch (device_type) {
9696
case torch_kCPU:
9797
if (device_index != -1) {
98-
ctorch_warn("device index unused for CPU-only runs");
98+
ctorch_warn("device index unused for tensors on CPUs");
9999
}
100100
return torch::Device(torch::kCPU);
101101
#if (GPU_DEVICE == GPU_DEVICE_CUDA) || (GPU_DEVICE == GPU_DEVICE_HIP)

test/unit/tensor/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ if("${GPU_DEVICE}" STREQUAL "CUDA" OR "${GPU_DEVICE}" STREQUAL "HIP")
4242
message(WARNING "No HIP support")
4343
endif()
4444
endif()
45+
add_pfunit_ctest(unittest_tensor_constructors_destructors_cuda
46+
TEST_SOURCES unittest_tensor_constructors_destructors_cuda.pf
47+
LINK_LIBRARIES FTorch::ftorch)
4548
add_pfunit_ctest(unittest_tensor_interrogation_cuda
4649
TEST_SOURCES unittest_tensor_interrogation_cuda.pf
4750
LINK_LIBRARIES FTorch::ftorch)
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
!| Unit tests for FTorch subroutines that construct and destroy tensors on CUDA
2+
! devices.
3+
!
4+
! * License
5+
! FTorch is released under an MIT license.
6+
! See the [LICENSE](https://github.com/Cambridge-ICCS/FTorch/blob/main/LICENSE)
7+
! file for details.
8+
module unittest_tensor_constructors_destructors_cuda
9+
use funit
10+
use ftorch_devices, only: torch_kCPU, torch_kCUDA
11+
use ftorch_types, only: torch_kFloat32
12+
use ftorch_tensor, only: assignment(=), torch_tensor, torch_tensor_delete, &
13+
torch_tensor_from_array, torch_tensor_to
14+
use ftorch_test_utils, only: allclose
15+
use, intrinsic :: iso_fortran_env, only: sp => real32
16+
use iso_c_binding, only: c_associated, c_int64_t
17+
18+
implicit none
19+
20+
public
21+
22+
! Set working precision for reals
23+
integer, parameter :: wp = sp
24+
25+
! All unit tests in this module run on a single CUDA device with a CPU host
26+
integer, parameter :: host_type = torch_kCPU
27+
integer, parameter :: device_type = torch_kCUDA
28+
integer, parameter :: device_index = 0
29+
30+
! All unit tests in this module use float32 precision
31+
integer, parameter :: dtype = torch_kFloat32
32+
33+
! Typedef holding a set of parameter values
34+
@testParameter
35+
type, extends(AbstractTestParameter) :: TestParametersType
36+
logical :: requires_grad ! Value used for the requires_grad argument
37+
logical :: auto_delete ! torch_tensor_delete is called when .false., otherwise the finalizer
38+
! will call it when a tensor goes out of scope
39+
integer :: iterations ! Number of times to construct/destruct a tensor
40+
contains
41+
procedure :: toString
42+
end type TestParametersType
43+
44+
! Typedef for a test case with a particular set of parameters
45+
@testCase(constructor=test_case_constructor)
46+
type, extends (ParameterizedTestCase) :: TestCaseType
47+
type(TestParametersType) :: param
48+
end type TestCaseType
49+
50+
contains
51+
52+
! Constructor for the test case type
53+
function test_case_constructor(param)
54+
type(TestCaseType) :: test_case_constructor
55+
type(TestParametersType), intent(in) :: param
56+
test_case_constructor%param = param
57+
end function test_case_constructor
58+
59+
! A fixture comprised of parameter sets for destructor tests
60+
function get_parameters_destruction() result(params)
61+
type(TestParametersType), allocatable :: params(:)
62+
params = [ &
63+
TestParametersType(.false.,.false.,1), &
64+
TestParametersType(.false.,.false.,2), &
65+
TestParametersType(.false.,.true.,1), &
66+
TestParametersType(.false.,.true.,2) &
67+
]
68+
end function get_parameters_destruction
69+
70+
! A fixture comprised of parameter sets for varying the requires_grad argument
71+
function get_parameters_requires_grad() result(params)
72+
type(TestParametersType), allocatable :: params(:)
73+
params = [ &
74+
TestParametersType(.false.,.false.,1), &
75+
TestParametersType(.true.,.false.,1) &
76+
]
77+
end function get_parameters_requires_grad
78+
79+
! Function for representing a parameter set as a string
80+
function toString(this) result(string)
81+
class(TestParametersType), intent(in) :: this
82+
character(:), allocatable :: string
83+
character(len=7) :: str
84+
write(str,"(l1,',',l1,',',i1)") this%requires_grad, this%auto_delete, this%iterations
85+
string = str
86+
end function toString
87+
88+
! Unit test for the torch_tensor_empty subroutine
89+
@test(testparameters={get_parameters_requires_grad()})
90+
subroutine test_empty(this)
91+
use ftorch_tensor, only: torch_tensor_empty
92+
93+
implicit none
94+
95+
class(TestCaseType), intent(inout) :: this
96+
type(torch_tensor) :: gpu_tensor
97+
integer, parameter :: ndims = 2
98+
integer(c_int64_t), dimension(2), parameter :: tensor_shape = [2, 3]
99+
integer(c_int64_t), parameter :: expected_stride(ndims) = [3, 1]
100+
101+
! Check the tensor pointer is not associated
102+
@assertFalse(c_associated(gpu_tensor%p))
103+
104+
! Create a tensor without any data values assigned on the CUDA device
105+
call torch_tensor_empty(gpu_tensor, ndims, tensor_shape, dtype, device_type, device_index, &
106+
this%param%requires_grad)
107+
108+
! Check the tensor pointer is associated
109+
@assertTrue(c_associated(gpu_tensor%p))
110+
111+
! Check the tensor properties
112+
@assertEqual(expected_stride, gpu_tensor%get_stride())
113+
@assertEqual(tensor_shape, gpu_tensor%get_shape())
114+
@assertEqual(device_type, gpu_tensor%get_device_type())
115+
@assertEqual(device_index, gpu_tensor%get_device_index())
116+
117+
end subroutine test_empty
118+
119+
! Unit test for the torch_tensor_zeros subroutine
120+
@test(testParameters={get_parameters_requires_grad()})
121+
subroutine test_zeros(this)
122+
use ftorch_tensor, only: torch_tensor_zeros
123+
124+
implicit none
125+
126+
class(TestCaseType), intent(inout) :: this
127+
type(torch_tensor) :: cpu_tensor, gpu_tensor
128+
integer, parameter :: ndims = 2
129+
integer(c_int64_t), parameter :: tensor_shape(ndims) = [2, 3]
130+
integer(c_int64_t), parameter :: expected_stride(ndims) = [3, 1]
131+
real(wp), dimension(2,3), target :: out_data
132+
real(wp), dimension(2,3) :: expected
133+
134+
! Check the tensor pointer is not associated
135+
@assertFalse(c_associated(gpu_tensor%p))
136+
137+
! Create a tensor of zeros on the CUDA device
138+
call torch_tensor_zeros(gpu_tensor, ndims, tensor_shape, dtype, device_type, device_index, &
139+
this%param%requires_grad)
140+
141+
! Check the tensor pointer is associated
142+
@assertTrue(c_associated(gpu_tensor%p))
143+
144+
! Check the tensor properties
145+
@assertEqual(expected_stride, gpu_tensor%get_stride())
146+
@assertEqual(tensor_shape, gpu_tensor%get_shape())
147+
@assertEqual(device_type, gpu_tensor%get_device_type())
148+
@assertEqual(device_index, gpu_tensor%get_device_index())
149+
150+
! Create a tensor based off an output array on the CPU host
151+
call torch_tensor_from_array(cpu_tensor, out_data, host_type)
152+
153+
! Transfer data from the device to the host
154+
call torch_tensor_to(gpu_tensor, cpu_tensor)
155+
156+
! Check that the tensor values are all zero
157+
expected(:,:) = 0.0
158+
@assertTrue(allclose(out_data, expected, test_name="test_zeros"))
159+
160+
end subroutine test_zeros
161+
162+
! Unit test for the torch_tensor_ones subroutine
163+
@test(testParameters={get_parameters_requires_grad()})
164+
subroutine test_ones(this)
165+
use ftorch_tensor, only: torch_tensor_ones
166+
167+
implicit none
168+
169+
class(TestCaseType), intent(inout) :: this
170+
type(torch_tensor) :: cpu_tensor, gpu_tensor
171+
integer, parameter :: ndims = 2
172+
integer(c_int64_t), parameter :: tensor_shape(ndims) = [2, 3]
173+
integer(c_int64_t), parameter :: expected_stride(ndims) = [3, 1]
174+
real(wp), dimension(2,3), target :: out_data
175+
real(wp), dimension(2,3) :: expected
176+
177+
! Check the tensor pointer is not associated
178+
@assertFalse(c_associated(gpu_tensor%p))
179+
180+
! Create tensor of ones on the CUDA device
181+
call torch_tensor_ones(gpu_tensor, ndims, tensor_shape, dtype, device_type, device_index, &
182+
this%param%requires_grad)
183+
184+
! Check the tensor pointer is associated
185+
@assertTrue(c_associated(gpu_tensor%p))
186+
187+
! Check the tensor properties
188+
@assertEqual(expected_stride, gpu_tensor%get_stride())
189+
@assertEqual(tensor_shape, gpu_tensor%get_shape())
190+
@assertEqual(device_type, gpu_tensor%get_device_type())
191+
@assertEqual(device_index, gpu_tensor%get_device_index())
192+
193+
! Create a tensor based off an output array on the CPU host
194+
call torch_tensor_from_array(cpu_tensor, out_data, host_type)
195+
196+
! Transfer data from the device to the host
197+
call torch_tensor_to(gpu_tensor, cpu_tensor)
198+
199+
! Check that the tensor values are all one
200+
expected(:,:) = 1.0
201+
@assertTrue(allclose(out_data, expected, test_name="test_ones"))
202+
203+
end subroutine test_ones
204+
205+
! Unit test for destroying tensors, both manually with torch_tensor_delete and automatically (via
206+
! torch_tensor's destructor)
207+
@test(testparameters={get_parameters_destruction()})
208+
subroutine test_destruction(this)
209+
use ftorch_tensor, only: torch_tensor_empty
210+
211+
implicit none
212+
213+
class(TestCaseType), intent(inout) :: this
214+
type(torch_tensor) :: tensor
215+
integer, parameter :: ndims = 2
216+
integer(c_int64_t), dimension(2), parameter :: tensor_shape = [2, 3]
217+
integer :: i
218+
219+
do i = 1, this%param%iterations
220+
221+
! Check the tensor pointer is not associated
222+
@assertFalse(c_associated(tensor%p))
223+
224+
! Create a tensor without any data values assigned
225+
call torch_tensor_empty(tensor, ndims, tensor_shape, dtype, device_type, device_index)
226+
227+
! Check the tensor pointer is associated
228+
@assertTrue(c_associated(tensor%p))
229+
230+
if (i < this%param%iterations .or. .not. this%param%auto_delete) then
231+
! Call torch_tensor_delete manually
232+
call torch_tensor_delete(tensor)
233+
234+
! Check torch_tensor_delete does indeed free the memory
235+
@assertFalse(c_associated(tensor%p))
236+
end if
237+
238+
end do
239+
240+
end subroutine test_destruction
241+
242+
end module unittest_tensor_constructors_destructors_cuda

0 commit comments

Comments
 (0)