Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 11e8b0b

Browse files
OuadiElfaroukis-Nickhjabirdpgorlani
authored
Fixed issues encountered through oneMKL portBLAS backend (#504)
* minor fixes and reverting of ACPP changes causing unexpected tests & header only lib behaviors * fixes to gemm half support with default cpu * Add check for managed usm allocation for AMD * Added clarifications regarding half support Co-authored-by: nscipione <[email protected]> Co-authored-by: HJA Bird <[email protected]> Co-authored-by: pgorlani <[email protected]>
1 parent 5783414 commit 11e8b0b

File tree

15 files changed

+327
-135
lines changed

15 files changed

+327
-135
lines changed

CMakeLists.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,6 @@ option(BLAS_ENABLE_EXTENSIONS "Whether to enable portBLAS extensions" ON)
113113
option(BLAS_ENABLE_COMPLEX "Whether to enable complex data type for GEMM" OFF)
114114
option(BLAS_ENABLE_HALF "Whether to enable sycl::half data type for supported operators" OFF)
115115

116-
if(((NOT INSTALL_HEADER_ONLY) AND (TUNING_TARGET STREQUAL "DEFAULT_CPU"))
117-
OR (INSTALL_HEADER_ONLY AND (NOT TUNING_TARGET)))
118-
set(BLAS_ENABLE_HALF OFF)
119-
message(STATUS "FP16 operations are not supported for CPU targets. BLAS_ENABLE_HALF is disabled")
120-
endif()
121-
122116
if (SYCL_COMPILER MATCHES "adaptivecpp")
123117
if(BLAS_ENABLE_COMPLEX)
124118
message(STATUS "SYCL Complex data is not supported on AdaptiveCpp/hipSYCL. Complex

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,10 +491,10 @@ Some of the supported options are:
491491
| `BLAS_MEMPOOL_BENCHMARK` | `ON`/`OFF` | Determines whether to enable the scratchpad memory pool for benchmark execution. `OFF` by default |
492492
| `BLAS_ENABLE_CONST_INPUT` | `ON`/`OFF` | Determines whether to enable kernel instantiation with const input buffer (`ON` by default) |
493493
| `BLAS_ENABLE_EXTENSIONS` | `ON`/`OFF` | Determines whether to enable portBLAS extensions (`ON` by default) |
494-
| `BLAS_DATA_TYPES` | `float;double` | Determines the floating-point types to instantiate BLAS operations for. Default is `float` |
495-
| `BLAS_INDEX_TYPES` | `int32_t;int64_t` | Determines the type(s) to use for `index_t` and `increment_t`. Default is `int` |
494+
| `BLAS_DATA_TYPES` | `float;double` | Determines the floating-point types to instantiate BLAS operations for. Default is `float`. Enabling other types such as complex or half requires setting their respective options *(next)*. |
496495
| `BLAS_ENABLE_COMPLEX` | `ON`/`OFF` | Determines whether to enable Complex data type support *(GEMM Operators only)* (`OFF` by default) |
497496
| `BLAS_ENABLE_HALF` | `ON`/`OFF` | Determines whether to enable Half data type support *(Support is limited to some Level 1 operators and Gemm)* (`OFF` by default) |
497+
| `BLAS_INDEX_TYPES` | `int32_t;int64_t` | Determines the type(s) to use for `index_t` and `increment_t`. Default is `int` |
498498

499499
## ComputeCpp Compilation *(Deprecated)*
500500

cmake/CmakeFunctionHelper.cmake

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,15 @@ else() # default cpu backend
702702
add_gemm_configuration(
703703
"${data}" 64 "false" "false" "false"
704704
64 2 2 4 4 1 1 1 1 4 4 1 1 1 float float "no_local" "standard" "full" 4 "interleaved" "false" "false")
705+
706+
if(BLAS_ENABLE_HALF)
707+
add_gemm_configuration(
708+
"half" 128 "false" "false" "false"
709+
64 4 4 8 8 1 1 1 1 1 1 1 1 1 float float "no_local" "standard" "full" 1 "strided" "false" "false")
710+
add_gemm_configuration(
711+
"half" 64 "false" "false" "false"
712+
64 2 2 4 4 1 1 1 1 4 4 1 1 1 float float "no_local" "standard" "full" 4 "interleaved" "false" "false")
713+
endif()
705714
endforeach()
706715

707716
if(BLAS_ENABLE_COMPLEX)

cmake/Modules/SYCL.cmake

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ include(CheckCXXCompilerFlag)
2626
include(ConfigurePORTBLAS)
2727

2828
# find_package(AdaptiveCpp) requires ACPP_TARGETS to be set, so set it to a default value before find_package(AdaptiveCpp)
29-
if(SYCL_COMPILER MATCHES "adaptivecpp" AND NOT ACPP_TARGETS AND NOT ENV{ACPP_TARGETS})
30-
message(STATUS "Using `omp` as ACPP_TARGETS")
31-
set(ACPP_TARGETS "omp")
32-
else()
33-
message(STATUS "Using ${ACPP_TARGETS} as ACPP_TARGETS")
29+
if(SYCL_COMPILER MATCHES "adaptivecpp")
30+
if(NOT ACPP_TARGETS AND NOT ENV{ACPP_TARGETS})
31+
message(STATUS "Using `omp` as ACPP_TARGETS")
32+
set(ACPP_TARGETS "omp")
33+
else()
34+
message(STATUS "Using ${ACPP_TARGETS} as ACPP_TARGETS")
35+
endif()
3436
endif()
3537

3638
check_cxx_compiler_flag("--acpp-targets" has_acpp)

include/container/sycl_iterator.h

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -194,51 +194,27 @@ template <cl::sycl::access::mode acc_md_t>
194194
inline typename BufferIterator<element_t>::template accessor_t<acc_md_t>
195195
BufferIterator<element_t>::get_range_accessor(cl::sycl::handler& cgh,
196196
size_t size) {
197-
if constexpr (acc_md_t == cl::sycl::access::mode::read) {
198-
return typename BufferIterator<element_t>::template accessor_t<acc_md_t>(
199-
buffer_, cgh, cl::sycl::range<1>(size),
200-
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()));
201-
} else {
202-
// Skip data initialization if not accessing in read mode only
203-
return typename BufferIterator<element_t>::template accessor_t<acc_md_t>(
204-
buffer_, cgh, cl::sycl::range<1>(size),
205-
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()),
206-
cl::sycl::property::no_init{});
207-
}
197+
return typename BufferIterator<element_t>::template accessor_t<acc_md_t>(
198+
buffer_, cgh, cl::sycl::range<1>(size),
199+
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()));
208200
}
209201

210202
template <typename element_t>
211203
template <cl::sycl::access::mode acc_md_t>
212204
inline typename BufferIterator<element_t>::template accessor_t<acc_md_t>
213205
BufferIterator<element_t>::get_range_accessor(cl::sycl::handler& cgh) {
214-
if constexpr (acc_md_t == cl::sycl::access::mode::read) {
215-
return BufferIterator<element_t>::get_range_accessor<acc_md_t>(
216-
cgh, BufferIterator<element_t>::get_size());
217-
} else {
218-
// Skip data initialization if not accessing in read mode only
219-
return BufferIterator<element_t>::get_range_accessor<acc_md_t>(
220-
cgh, BufferIterator<element_t>::get_size(),
221-
cl::sycl::property::no_init{});
222-
}
206+
return BufferIterator<element_t>::get_range_accessor<acc_md_t>(
207+
cgh, BufferIterator<element_t>::get_size());
223208
}
224209

225210
template <typename element_t>
226211
template <cl::sycl::access::mode acc_md_t>
227212
inline typename BufferIterator<element_t>::template placeholder_accessor_t<
228213
acc_md_t>
229214
BufferIterator<element_t>::get_range_accessor(size_t size) {
230-
if constexpr (acc_md_t == cl::sycl::access::mode::read) {
231-
return typename BufferIterator<element_t>::template placeholder_accessor_t<
232-
acc_md_t>(buffer_, cl::sycl::range<1>(size),
233-
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()));
234-
235-
} else {
236-
// Skip data initialization if not accessing in read mode only
237-
return typename BufferIterator<element_t>::template placeholder_accessor_t<
238-
acc_md_t>(buffer_, cl::sycl::range<1>(size),
239-
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()),
240-
cl::sycl::property::no_init{});
241-
}
215+
return typename BufferIterator<element_t>::template placeholder_accessor_t<
216+
acc_md_t>(buffer_, cl::sycl::range<1>(size),
217+
cl::sycl::id<1>(BufferIterator<element_t>::get_offset()));
242218
}
243219

244220
template <typename element_t>

include/interface/blas1_interface.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ typename sb_handle_t::event_t _asum(
136136
* \brief Prototype for the internal implementation of the ASUM operation. See
137137
* documentation in the blas1_interface.hpp file for details.
138138
*/
139-
template <int localSize, int localMemSize, typename sb_handle_t,
140-
typename container_0_t, typename container_1_t, typename index_t,
141-
typename increment_t>
139+
template <int localSize, int localMemSize, bool usmManagedMem = false,
140+
typename sb_handle_t, typename container_0_t, typename container_1_t,
141+
typename index_t, typename increment_t>
142142
typename sb_handle_t::event_t _asum_impl(
143143
sb_handle_t &sb_handle, index_t _N, container_0_t _vx, increment_t _incx,
144144
container_1_t _rs, const index_t number_WG,
@@ -257,9 +257,9 @@ typename sb_handle_t::event_t _nrm2(
257257
* \brief Prototype for the internal implementation of the NRM2 operator. See
258258
* documentation in the blas1_interface.hpp file for details.
259259
*/
260-
template <int localSize, int localMemSize, typename sb_handle_t,
261-
typename container_0_t, typename container_1_t, typename index_t,
262-
typename increment_t>
260+
template <int localSize, int localMemSize, bool usmManagedMem = false,
261+
typename sb_handle_t, typename container_0_t, typename container_1_t,
262+
typename index_t, typename increment_t>
263263
typename sb_handle_t::event_t _nrm2_impl(
264264
sb_handle_t &sb_handle, index_t _N, container_0_t _vx, increment_t _incx,
265265
container_1_t _rs, const index_t number_WG,
@@ -269,8 +269,8 @@ typename sb_handle_t::event_t _nrm2_impl(
269269
* \brief Prototype for the internal implementation of the Dot operator. See
270270
* documentation in the blas1_interface.hpp file for details.
271271
*/
272-
template <int localSize, int localMemSize, typename sb_handle_t,
273-
typename container_0_t, typename container_1_t,
272+
template <int localSize, int localMemSize, bool usmManagedMem = false,
273+
typename sb_handle_t, typename container_0_t, typename container_1_t,
274274
typename container_2_t, typename index_t, typename increment_t>
275275
typename sb_handle_t::event_t _dot_impl(
276276
sb_handle_t &sb_handle, index_t _N, container_0_t _vx, increment_t _incx,

include/operations/blas1_trees.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ struct AssignReduction {
208208
* function below.
209209
*
210210
*/
211-
template <typename operator_t, typename lhs_t, typename rhs_t>
211+
template <typename operator_t, bool usmManagedMem, typename lhs_t,
212+
typename rhs_t>
212213
struct WGAtomicReduction {
213214
using value_t = typename lhs_t::value_t;
214215
using index_t = typename rhs_t::index_t;
@@ -304,10 +305,11 @@ inline AssignReduction<operator_t, lhs_t, rhs_t> make_assign_reduction(
304305
lhs_, rhs_, local_num_thread_, global_num_thread_);
305306
}
306307

307-
template <typename operator_t, typename lhs_t, typename rhs_t>
308-
inline WGAtomicReduction<operator_t, lhs_t, rhs_t> make_wg_atomic_reduction(
309-
lhs_t &lhs_, rhs_t &rhs_) {
310-
return WGAtomicReduction<operator_t, lhs_t, rhs_t>(lhs_, rhs_);
308+
template <typename operator_t, bool usmManagedMem = false, typename lhs_t,
309+
typename rhs_t>
310+
inline WGAtomicReduction<operator_t, usmManagedMem, lhs_t, rhs_t>
311+
make_wg_atomic_reduction(lhs_t &lhs_, rhs_t &rhs_) {
312+
return WGAtomicReduction<operator_t, usmManagedMem, lhs_t, rhs_t>(lhs_, rhs_);
311313
}
312314

313315
template <bool is_max, bool is_step0, typename lhs_t, typename rhs_t>

include/portblas_helper.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ inline cl::sycl::event fill(cl::sycl::queue q, element_t *buff, element_t value,
220220
}
221221
#endif
222222

223+
template <typename sb_handle_t, typename containerT>
224+
inline bool is_malloc_shared(sb_handle_t &sb_handle, const containerT _rs) {
225+
if constexpr (std::is_pointer_v<containerT>) {
226+
return sycl::usm::alloc::shared ==
227+
sycl::get_pointer_type(_rs, sb_handle.get_queue().get_context());
228+
} else {
229+
return false;
230+
}
231+
}
232+
223233
} // end namespace helper
224234
} // end namespace blas
225235
#endif // PORTBLAS_HELPER_H

samples/CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
#/***************************************************************************
2+
# *
3+
# * @license
4+
# * Copyright (C) Codeplay Software Limited
5+
# * Licensed under the Apache License, Version 2.0 (the "License");
6+
# * you may not use this file except in compliance with the License.
7+
# * You may obtain a copy of the License at
8+
# *
9+
# * http://www.apache.org/licenses/LICENSE-2.0
10+
# *
11+
# * For your convenience, a copy of the License has been included in this
12+
# * repository.
13+
# *
14+
# * Unless required by applicable law or agreed to in writing, software
15+
# * distributed under the License is distributed on an "AS IS" BASIS,
16+
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# * See the License for the specific language governing permissions and
18+
# * limitations under the License.
19+
# *
20+
# * portBLAS: BLAS implementation using SYCL
21+
# *
22+
# * @filename CMakeLists.txt
23+
# *
24+
# **************************************************************************/
25+
cmake_minimum_required(VERSION 3.4.3)
26+
27+
project(portBLASSample LANGUAGES CXX)
28+
129
set(PORTBLAS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include)
230
set(PORTBLAS_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src)
331
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})

src/interface/blas1/backend/amd_gpu.hpp

Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#ifndef PORTBLAS_ASUM_AMD_GPU_BACKEND_HPP
2626
#define PORTBLAS_ASUM_AMD_GPU_BACKEND_HPP
2727
#include "interface/blas1_interface.h"
28+
#include "portblas_helper.h"
2829

2930
namespace blas {
3031
namespace asum {
@@ -34,16 +35,42 @@ template <typename sb_handle_t, typename container_0_t, typename container_1_t,
3435
typename sb_handle_t::event_t _asum(
3536
sb_handle_t& sb_handle, index_t _N, container_0_t _vx, increment_t _incx,
3637
container_1_t _rs, const typename sb_handle_t::event_t& _dependencies) {
37-
if (_N < (1 << 18)) {
38-
constexpr index_t localSize = 1024;
39-
const index_t number_WG = (_N + localSize - 1) / localSize;
40-
return blas::internal::_asum_impl<static_cast<int>(localSize), 32>(
41-
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
38+
/**
39+
* This compile time check is absolutely necessary for AMD GPUs.
40+
* AMD's atomic operations require a specific combination of hardware that
41+
* cannot be checked nor enforced. Since the reduction operator kernel
42+
* implementation uses atomic operations, without that particular hardware
43+
* combination the reduction may silently fail.
44+
**/
45+
#ifdef SB_ENABLE_USM
46+
const bool usm_managed_mem = blas::helper::is_malloc_shared(sb_handle, _rs);
47+
#else
48+
constexpr bool usm_managed_mem{false};
49+
#endif
50+
if (usm_managed_mem) {
51+
if (_N < (1 << 18)) {
52+
constexpr index_t localSize = 1024;
53+
const index_t number_WG = (_N + localSize - 1) / localSize;
54+
return blas::internal::_asum_impl<static_cast<int>(localSize), 32, true>(
55+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
56+
} else {
57+
constexpr int localSize = 512;
58+
constexpr index_t number_WG = 256;
59+
return blas::internal::_asum_impl<localSize, 32, true>(
60+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
61+
}
4262
} else {
43-
constexpr int localSize = 512;
44-
constexpr index_t number_WG = 256;
45-
return blas::internal::_asum_impl<localSize, 32>(
46-
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
63+
if (_N < (1 << 18)) {
64+
constexpr index_t localSize = 1024;
65+
const index_t number_WG = (_N + localSize - 1) / localSize;
66+
return blas::internal::_asum_impl<static_cast<int>(localSize), 32, false>(
67+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
68+
} else {
69+
constexpr int localSize = 512;
70+
constexpr index_t number_WG = 256;
71+
return blas::internal::_asum_impl<localSize, 32, false>(
72+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
73+
}
4774
}
4875
}
4976
} // namespace backend
@@ -101,16 +128,42 @@ template <typename sb_handle_t, typename container_0_t, typename container_1_t,
101128
typename sb_handle_t::event_t _nrm2(
102129
sb_handle_t& sb_handle, index_t _N, container_0_t _vx, increment_t _incx,
103130
container_1_t _rs, const typename sb_handle_t::event_t& _dependencies) {
104-
if (_N < (1 << 18)) {
105-
constexpr index_t localSize = 1024;
106-
const index_t number_WG = (_N + localSize - 1) / localSize;
107-
return blas::internal::_nrm2_impl<static_cast<int>(localSize), 32>(
108-
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
131+
/**
132+
* This compile time check is absolutely necessary for AMD GPUs.
133+
* AMD's atomic operations require a specific combination of hardware that
134+
* cannot be checked nor enforced. Since the reduction operator kernel
135+
* implementation uses atomic operations, without that particular hardware
136+
* combination the reduction may silently fail.
137+
**/
138+
#ifdef SB_ENABLE_USM
139+
const bool usm_managed_mem = blas::helper::is_malloc_shared(sb_handle, _rs);
140+
#else
141+
constexpr bool usm_managed_mem{false};
142+
#endif
143+
if (usm_managed_mem) {
144+
if (_N < (1 << 18)) {
145+
constexpr index_t localSize = 1024;
146+
const index_t number_WG = (_N + localSize - 1) / localSize;
147+
return blas::internal::_nrm2_impl<static_cast<int>(localSize), 32, true>(
148+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
149+
} else {
150+
constexpr int localSize = 512;
151+
constexpr index_t number_WG = 512;
152+
return blas::internal::_nrm2_impl<localSize, 32, true>(
153+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
154+
}
109155
} else {
110-
constexpr int localSize = 512;
111-
constexpr index_t number_WG = 512;
112-
return blas::internal::_nrm2_impl<localSize, 32>(
113-
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
156+
if (_N < (1 << 18)) {
157+
constexpr index_t localSize = 1024;
158+
const index_t number_WG = (_N + localSize - 1) / localSize;
159+
return blas::internal::_nrm2_impl<static_cast<int>(localSize), 32, false>(
160+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
161+
} else {
162+
constexpr int localSize = 512;
163+
constexpr index_t number_WG = 512;
164+
return blas::internal::_nrm2_impl<localSize, 32, false>(
165+
sb_handle, _N, _vx, _incx, _rs, number_WG, _dependencies);
166+
}
114167
}
115168
}
116169
} // namespace backend
@@ -124,16 +177,42 @@ typename sb_handle_t::event_t _dot(
124177
sb_handle_t& sb_handle, index_t _N, container_0_t _vx, increment_t _incx,
125178
container_1_t _vy, increment_t _incy, container_2_t _rs,
126179
const typename sb_handle_t::event_t& _dependencies) {
127-
if (_N < (1 << 18)) {
128-
constexpr index_t localSize = 1024;
129-
const index_t number_WG = (_N + localSize - 1) / localSize;
130-
return blas::internal::_dot_impl<static_cast<int>(localSize), 32>(
131-
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
180+
/**
181+
* This compile time check is absolutely necessary for AMD GPUs.
182+
* AMD's atomic operations require a specific combination of hardware that
183+
* cannot be checked nor enforced. Since the reduction operator kernel
184+
* implementation uses atomic operations, without that particular hardware
185+
* combination the reduction may silently fail.
186+
**/
187+
#ifdef SB_ENABLE_USM
188+
const bool usm_managed_mem = blas::helper::is_malloc_shared(sb_handle, _rs);
189+
#else
190+
constexpr bool usm_managed_mem{false};
191+
#endif
192+
if (usm_managed_mem) {
193+
if (_N < (1 << 18)) {
194+
constexpr index_t localSize = 1024;
195+
const index_t number_WG = (_N + localSize - 1) / localSize;
196+
return blas::internal::_dot_impl<static_cast<int>(localSize), 32, true>(
197+
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
198+
} else {
199+
constexpr int localSize = 512;
200+
constexpr index_t number_WG = 512;
201+
return blas::internal::_dot_impl<localSize, 32, true>(
202+
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
203+
}
132204
} else {
133-
constexpr int localSize = 512;
134-
constexpr index_t number_WG = 512;
135-
return blas::internal::_dot_impl<localSize, 32>(
136-
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
205+
if (_N < (1 << 18)) {
206+
constexpr index_t localSize = 1024;
207+
const index_t number_WG = (_N + localSize - 1) / localSize;
208+
return blas::internal::_dot_impl<static_cast<int>(localSize), 32, false>(
209+
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
210+
} else {
211+
constexpr int localSize = 512;
212+
constexpr index_t number_WG = 512;
213+
return blas::internal::_dot_impl<localSize, 32, false>(
214+
sb_handle, _N, _vx, _incx, _vy, _incy, _rs, number_WG, _dependencies);
215+
}
137216
}
138217
}
139218
} // namespace backend

0 commit comments

Comments
 (0)