Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
linalg/backends/cuda/copy.hpp
Go to the documentation of this file.
1#pragma once
2
11#include <uni20/core/math.hpp>
21
22#include <cuda_runtime_api.h>
23
24#include <concepts>
25#include <cstddef>
26#include <limits>
27#include <memory>
28#include <type_traits>
29#include <utility>
30#include <vector>
31
32namespace uni20::linalg
33{
34namespace detail::cuda_reference
35{
36
37enum class CopyDirection
38{
39 host_to_device,
40 device_to_host,
41 device_to_device
42};
43
44enum class CopyExecution
45{
46 runtime_copy,
47 elementwise_kernel
48};
49
50template <class Accessor> struct IsConjugatedCudaAccessor : std::false_type
51{};
52
53template <class Accessor>
54struct IsConjugatedCudaAccessor<uni20::conjugated_accessor<Accessor>>
55 : std::bool_constant<RawCudaAccessorTraits<Accessor>::is_raw>
56{};
57
58template <class Accessor> struct IsConjugatedHostAccessor : std::false_type
59{};
60
61template <class Accessor>
62struct IsConjugatedHostAccessor<uni20::conjugated_accessor<Accessor>>
63 : std::bool_constant<uni20::is_default_accessor_v<Accessor>>
64{};
65
66template <class Mdspan> inline constexpr bool is_cuda_mdspan = uni20::cuda::BufferMdspec<Mdspan>;
67
68template <class Mdspan>
69inline constexpr bool is_conjugated_cuda_mdspan =
70 is_cuda_mdspan<Mdspan> && IsConjugatedCudaAccessor<typename std::remove_cvref_t<Mdspan>::accessor_type>::value;
71
72template <class Mdspan>
73inline constexpr bool is_supported_cuda_mdspan = is_raw_cuda_mdspec<Mdspan> || is_conjugated_cuda_mdspan<Mdspan>;
74
75template <class Mdspan>
76inline constexpr bool is_raw_host_mdspan = uni20::DefaultAccessorMdspanLike<std::remove_cvref_t<Mdspan>>;
77
78template <class Mdspan>
79inline constexpr bool is_conjugated_host_mdspan =
80 uni20::MdspanLike<Mdspan> && IsConjugatedHostAccessor<typename std::remove_cvref_t<Mdspan>::accessor_type>::value;
81
82template <class Mdspan>
83inline constexpr bool is_supported_host_mdspan = is_raw_host_mdspan<Mdspan> || is_conjugated_host_mdspan<Mdspan>;
84
85template <class OutputMdspan, class InputMdspan>
86concept SupportedCopyMdspans =
90 (std::remove_cvref_t<OutputMdspan>::rank() == std::remove_cvref_t<InputMdspan>::rank()) &&
91 std::same_as<std::remove_cv_t<typename std::remove_cvref_t<OutputMdspan>::element_type>,
92 std::remove_cv_t<typename std::remove_cvref_t<InputMdspan>::element_type>> &&
93 ((is_raw_cuda_mdspec<OutputMdspan> &&
94 (is_supported_cuda_mdspan<InputMdspan> || is_supported_host_mdspan<InputMdspan>)) ||
95 (is_raw_host_mdspan<OutputMdspan> && is_supported_cuda_mdspan<InputMdspan>));
96
97template <class Scalar> struct CopyPlan
98{
100 CopyDirection direction = CopyDirection::device_to_device;
101 CopyExecution execution = CopyExecution::runtime_copy;
102 ElementwiseCopyTransform transform = ElementwiseCopyTransform::identity;
103 uni20::cuda::CudaBuffer<Scalar>* output_buffer = nullptr;
104 uni20::cuda::CudaBuffer<Scalar> const* input_buffer = nullptr;
105 Scalar* output_host = nullptr;
106 Scalar const* input_host = nullptr;
107 std::size_t output_offset = 0;
108 std::size_t input_offset = 0;
109 std::size_t element_count = 0;
110 std::size_t output_span_size = 0;
111 std::size_t input_span_size = 0;
112 LoweredElementwiseCopyPlan elementwise_plan;
113 std::vector<Scalar> input_staging;
114 bool conjugate_host_output = false;
115 bool has_work = false;
116};
117
118template <class Mapping> [[nodiscard]] bool mapping_is_contiguous(Mapping const& mapping)
119{
120 return mapping.is_unique() && mapping.is_exhaustive();
121}
122
123template <class Mdspan> [[nodiscard]] bool active_strides_are_positive(Mdspan const& span)
124{
125 for (std::size_t axis = 0; axis < std::remove_cvref_t<Mdspan>::rank(); ++axis)
126 {
127 if (span.extent(axis) == 0) return true;
128 }
129 for (std::size_t axis = 0; axis < std::remove_cvref_t<Mdspan>::rank(); ++axis)
130 {
131 if (span.extent(axis) > 1 && span.stride(axis) <= 0) return false;
132 }
133 return true;
134}
135
136template <class OutputMdspan, class InputMdspan>
137[[nodiscard]] bool physical_orders_match(OutputMdspan const& output, InputMdspan const& input)
138{
139 for (std::size_t axis = 0; axis < std::remove_cvref_t<OutputMdspan>::rank(); ++axis)
140 {
141 if (output.extent(axis) > 1 && output.stride(axis) != input.stride(axis)) return false;
142 }
143 return true;
144}
145
146template <class OutputMdspan, class InputMdspan>
147[[nodiscard]] bool try_make_elementwise_plan(OutputMdspan const& output, InputMdspan const& input,
148 LoweredElementwiseCopyPlan& plan)
149{
150 auto host_plan = make_multi_iteration_plan(std::tuple{output.mapping(), input.mapping()});
151 return try_lower_strided_elementwise_plan<elementwise_copy_maximum_rank>(host_plan, plan);
152}
153
154template <class Scalar>
155void validate_cuda_range(uni20::cuda::CudaBuffer<Scalar> const& buffer, std::size_t offset, std::size_t count)
156{
157 CHECK(offset <= buffer.size() && count <= buffer.size() - offset, offset, count, buffer.size());
158}
159
160template <uni20::cuda::BufferMdspec Mdspan> [[nodiscard]] auto cuda_buffer_view(Mdspan& span)
161{
162 return span.data_descriptor();
163}
164
165template <class OutputMdspan, class InputMdspan>
166 requires SupportedCopyMdspans<OutputMdspan, InputMdspan>
167[[nodiscard]] auto prepare_copy(OutputMdspan& output, InputMdspan& input)
168{
169 using output_type = std::remove_cvref_t<OutputMdspan>;
170 using scalar_type = std::remove_cv_t<typename output_type::element_type>;
171 CopyPlan<scalar_type> plan;
172
173 for (std::size_t axis = 0; axis < output_type::rank(); ++axis)
174 {
175 if (output.extent(axis) != input.extent(axis))
176 {
178 return plan;
179 }
180 }
181
182 if (!active_strides_are_positive(output) || !active_strides_are_positive(input))
183 {
185 return plan;
186 }
187
188 auto const output_required_span = output.mapping().required_span_size();
189 auto const input_required_span = input.mapping().required_span_size();
190 if (std::cmp_less(output_required_span, 0) ||
191 std::cmp_greater(output_required_span, std::numeric_limits<std::size_t>::max()) ||
192 std::cmp_less(input_required_span, 0) ||
193 std::cmp_greater(input_required_span, std::numeric_limits<std::size_t>::max()))
194 {
196 return plan;
197 }
198 plan.output_span_size = static_cast<std::size_t>(output_required_span);
199 plan.input_span_size = static_cast<std::size_t>(input_required_span);
200
201 if constexpr (is_cuda_mdspan<OutputMdspan> && is_cuda_mdspan<InputMdspan>)
202 {
203 plan.direction = CopyDirection::device_to_device;
204 }
205 else if constexpr (is_cuda_mdspan<OutputMdspan>)
206 {
207 plan.direction = CopyDirection::host_to_device;
208 }
209 else
210 {
211 plan.direction = CopyDirection::device_to_host;
212 }
213
214 bool const runtime_copy_eligible = mapping_is_contiguous(output.mapping()) &&
215 mapping_is_contiguous(input.mapping()) && physical_orders_match(output, input);
216 bool needs_elementwise_kernel = false;
217 if (!runtime_copy_eligible)
218 {
219 if constexpr (is_cuda_mdspan<OutputMdspan> && is_cuda_mdspan<InputMdspan>)
220 needs_elementwise_kernel = true;
221 else
222 {
224 return plan;
225 }
226 }
227
228 if constexpr (is_conjugated_cuda_mdspan<InputMdspan>)
229 {
230 if (plan.direction == CopyDirection::device_to_device)
231 needs_elementwise_kernel = true;
232 else
233 plan.conjugate_host_output = true;
234 }
235
236 if (needs_elementwise_kernel)
237 {
238 if constexpr (!supports_elementwise_copy<scalar_type>)
239 {
241 return plan;
242 }
243 if (!output.mapping().is_unique() || !try_make_elementwise_plan(output, input, plan.elementwise_plan))
244 {
246 return plan;
247 }
248 plan.execution = CopyExecution::elementwise_kernel;
249 plan.element_count = plan.elementwise_plan.element_count();
250 if constexpr (is_conjugated_cuda_mdspan<InputMdspan>) plan.transform = ElementwiseCopyTransform::conjugate;
251 }
252 else
253 {
254 if (plan.output_span_size != plan.input_span_size)
255 {
257 return plan;
258 }
259 plan.element_count = plan.output_span_size;
260 }
261
262 CHECK(plan.element_count <= std::numeric_limits<std::size_t>::max() / sizeof(scalar_type), plan.element_count,
263 sizeof(scalar_type));
264
265 if constexpr (is_cuda_mdspan<OutputMdspan>)
266 {
267 auto output_handle = cuda_buffer_view(output);
268 plan.output_buffer = std::addressof(output_handle.buffer());
269 plan.output_offset = output_handle.element_offset();
270 validate_cuda_range(*plan.output_buffer, plan.output_offset, plan.output_span_size);
271 }
272 else
273 {
274 plan.output_host = output.data_handle();
275 CHECK(plan.element_count == 0 || plan.output_host != nullptr);
276 }
277
278 if constexpr (is_cuda_mdspan<InputMdspan>)
279 {
280 auto input_handle = cuda_buffer_view(input);
281 plan.input_buffer = std::addressof(input_handle.buffer());
282 plan.input_offset = input_handle.element_offset();
283 validate_cuda_range(*plan.input_buffer, plan.input_offset, plan.input_span_size);
284 }
285 else
286 {
287 plan.input_host = input.data_handle();
288 CHECK(plan.element_count == 0 || plan.input_host != nullptr);
289 }
290
291 if (plan.element_count == 0)
292 {
293 plan.attempt = KernelAttempt::success;
294 return plan;
295 }
296
297 if constexpr (is_conjugated_host_mdspan<InputMdspan>)
298 {
299 CHECK(plan.direction == CopyDirection::host_to_device);
300 plan.input_staging.resize(plan.element_count);
301 for (std::size_t index = 0; index < plan.element_count; ++index)
302 plan.input_staging[index] = uni20::conj(plan.input_host[index]);
303 plan.input_host = plan.input_staging.data();
304 }
305
306 if constexpr (is_cuda_mdspan<OutputMdspan> && is_cuda_mdspan<InputMdspan>)
307 {
308 if (plan.output_buffer == plan.input_buffer)
309 {
310 if (plan.output_offset == plan.input_offset && plan.execution == CopyExecution::runtime_copy)
311 {
312 // Runtime-copy eligibility proves that both accessors observe the same
313 // values, so identical storage and offset require no work.
314 plan.attempt = KernelAttempt::success;
315 return plan;
316 }
317 if (plan.output_offset == plan.input_offset)
318 {
320 return plan;
321 }
322 }
323
324 if (plan.execution == CopyExecution::elementwise_kernel &&
325 plan.output_buffer->device().ordinal() != plan.input_buffer->device().ordinal())
326 {
328 return plan;
329 }
330 }
331
332 plan.attempt = KernelAttempt::success;
333 plan.has_work = plan.element_count != 0;
334 return plan;
335}
336
337template <class Scalar> void enqueue_device_copy(CopyPlan<Scalar> const& plan, uni20::cuda::Stream const& stream)
338{
339 CHECK(plan.direction == CopyDirection::device_to_device && plan.output_buffer != nullptr &&
340 plan.input_buffer != nullptr);
341 CHECK_EQUAL(stream.device(), plan.output_buffer->device().ordinal());
342
343 int const output_device = plan.output_buffer->device().ordinal();
344 int const input_device = plan.input_buffer->device().ordinal();
345 uni20::cuda::ScopedDevice guard(output_device);
346
347 auto enqueue = [&](Scalar* output_data, Scalar const* input_data) {
348 if (plan.execution == CopyExecution::elementwise_kernel)
349 {
350 CHECK_EQUAL(output_device, input_device);
351 if constexpr (supports_elementwise_copy<Scalar>)
352 {
353 plan.elementwise_plan.visit([&](auto const& elementwise_plan) {
354 enqueue_elementwise_copy(output_data, input_data, elementwise_plan, plan.transform, stream.native_handle(),
355 output_device);
356 });
357 return;
358 }
359 else
360 {
361 PANIC("unsupported scalar reached CUDA reference elementwise copy");
362 }
363 }
364
365 std::size_t const bytes = plan.element_count * sizeof(Scalar);
366 if (output_device == input_device)
367 {
369 cudaMemcpyAsync(output_data, input_data, bytes, cudaMemcpyDeviceToDevice, stream.native_handle()),
370 "cudaMemcpyAsync device-to-device", output_device);
371 }
372 else
373 {
375 cudaMemcpyPeerAsync(output_data, output_device, input_data, input_device, bytes, stream.native_handle()),
376 "cudaMemcpyPeerAsync", output_device);
377 }
378 };
379
380 if (plan.output_buffer == plan.input_buffer)
381 {
382 // C++ copy requires non-destructive overlap. One exclusive buffer access
383 // retains and orders both pointers when disjoint views share an allocation.
384 auto access = plan.output_buffer->write_synchronized_with(stream);
385 enqueue(access.data() + plan.output_offset, access.data() + plan.input_offset);
386 return;
387 }
388
389 auto output = plan.output_buffer->write_synchronized_with(stream);
390 auto input = plan.input_buffer->read_synchronized_with(stream);
391 enqueue(output.data() + plan.output_offset, input.data() + plan.input_offset);
392 uni20::cuda::AccessCompletion completion(stream);
393 completion.release(output);
394 completion.release(input);
395}
396
397template <class Scalar> void execute_blocking_copy(CopyPlan<Scalar> const& plan)
398{
399 CHECK(kernel_attempt_succeeded(plan.attempt));
400 if (!plan.has_work) return;
401
402 std::size_t const bytes = plan.element_count * sizeof(Scalar);
403 if (plan.direction == CopyDirection::device_to_device)
404 {
405 auto stream = plan.output_buffer->resources().streams().acquire();
406 enqueue_device_copy(plan, stream);
407 return;
408 }
409
410 if (plan.direction == CopyDirection::host_to_device)
411 {
412 auto output = plan.output_buffer->blocking_write_access();
413 int const device = plan.output_buffer->device().ordinal();
414 uni20::cuda::ScopedDevice guard(device);
415 uni20::cuda::check(cudaMemcpy(output.data() + plan.output_offset, plan.input_host, bytes, cudaMemcpyHostToDevice),
416 "cudaMemcpy host-to-device", device);
417 // Pageable H2D cudaMemcpy finishes consuming the host source after staging,
418 // but its default-stream DMA may remain outstanding. Publish that stream
419 // tail so later non-blocking streams observe the destination dependency.
420 try
421 {
422 output.release_with_completion(uni20::cuda::Completion::record(device, nullptr));
423 }
424 catch (...)
425 {
426 uni20::cuda::detail::synchronize_after_failed_publication(nullptr, device,
427 "publish pageable host-to-device completion");
428 throw;
429 }
430 return;
431 }
432
433 {
434 auto input = plan.input_buffer->blocking_read_access();
435 int const device = plan.input_buffer->device().ordinal();
436 uni20::cuda::ScopedDevice guard(device);
437 uni20::cuda::check(cudaMemcpy(plan.output_host, input.data() + plan.input_offset, bytes, cudaMemcpyDeviceToHost),
438 "cudaMemcpy device-to-host", device);
439 }
440 if (plan.conjugate_host_output)
441 {
442 for (std::size_t index = 0; index < plan.element_count; ++index)
443 plan.output_host[index] = uni20::conj(plan.output_host[index]);
444 }
445}
446
448template <uni20::MutableMdspecLike OutputMdspan, uni20::MdspecLike InputMdspan> consteval auto copy_acceptance()
449{
450 if constexpr (SupportedCopyMdspans<OutputMdspan, InputMdspan>)
451 return kernel_types_maybe;
452 else
453 return kernel_types_no;
454}
455
457template <class OutputMdspan, class InputMdspan>
458 requires SupportedCopyMdspans<OutputMdspan, InputMdspan>
459KernelAttempt copy(OutputMdspan&& output, InputMdspan&& input)
460{
461 auto preparation = prepare_copy(output, input);
462 if (!kernel_attempt_succeeded(preparation.attempt)) return preparation.attempt;
463 execute_blocking_copy(preparation);
465}
466} // namespace detail::cuda_reference
467
469template <uni20::MutableMdspecLike OutputMdspan, uni20::MdspecLike InputMdspan>
470consteval auto kernel_accepts_types(CudaReferenceBackend const&, copy_op const&, OutputMdspan&, InputMdspan&)
471{
472 constexpr auto acceptance = detail::cuda_reference::copy_acceptance<OutputMdspan, InputMdspan>();
473 if constexpr (acceptance == KernelTypeAcceptance::maybe)
474 return kernel_types_maybe;
475 else
476 return kernel_types_no;
477}
478
480template <uni20::MutableMdspecLike OutputMdspan, uni20::MdspecLike InputMdspan>
481KernelAttempt try_kernel(CudaReferenceBackend, copy_op const&, OutputMdspan& output, InputMdspan& input)
482{
483 return detail::cuda_reference::copy(output, input);
484}
485
486} // namespace uni20::linalg
Typed CUDA device buffers and scoped stream access guards.
One operation-tail completion shared by multiple CUDA buffer accesses.
Definition buffer.hpp:1160
static Completion record(int device, cudaStream_t stream)
Record a completion at the current tail of a native CUDA stream.
Definition runtime.cpp:312
Move-only logical CUDA buffer and completion state.
Definition buffer.hpp:222
std::size_t size() const noexcept
Return the number of typed elements in the allocation.
Definition buffer.hpp:260
Temporarily select one CUDA device and restore the previous device on destruction.
Definition runtime.hpp:44
Reference-counted lease of one CUDA stream-pool slot.
Definition runtime.hpp:65
An mdspan-like type whose accessor is stdex::default_accessor.
Definition mdspan/concepts.hpp:501
A readable type implementing the mdspan observers and multidimensional subscript contract.
Definition mdspan/concepts.hpp:291
MdspecLike types whose eventual storage supports writes.
Definition mdspan/concepts.hpp:398
An mdspec-like type whose mapping exposes multidimensional strides.
Definition mdspan/concepts.hpp:427
CUDA-accessible mdspec backed by a CudaBufferView descriptor.
Definition cuda_storage.hpp:118
Mdspan accessor adaptor for lazy complex conjugation.
Structured CUDA runtime failures and checked-call helpers.
CUDA Tensor storage with deferred device-memory data descriptors.
locale_t value
Definition data_table.cpp:49
Operation-tag backend dispatch over tensor-view operands.
Typed launch boundary for CUDA accessor-respecting element copies.
uni20::complex< T > conj(detail::standard_complex< T > x)
Returns the complex conjugate for complex-valued scalars.
Definition math.hpp:70
auto make_multi_iteration_plan(std::tuple< Mappings... > const &mappings)
Build a merged iteration plan for multiple tensors sharing the same extents.
Definition iteration_plan.hpp:263
Helpers for constructing merged iteration plans over strided tensors.
Scalar math helper utilities.
Mdspan concept and accessor extensions for Uni20.
Shared CUDA reference-backend traits for raw buffer mdspecs.
void check(cudaError_t status, std::string_view operation, std::optional< int > device, std::source_location where)
Raise when a CUDA runtime operation did not return cudaSuccess.
Definition cuda_error.cpp:44
stream
Selects the ordinary display stream for a message.
Definition display.hpp:25
void copy(std::span< Scalar > destination, std::span< Scalar const > source)
Copy a local dense vector.
Definition dense_linalg.hpp:183
Definition linalg/async.hpp:28
constexpr bool kernel_attempt_succeeded(KernelAttempt attempt) noexcept
Return whether a backend performed the operation.
Definition kernel_attempt.hpp:40
KernelAttempt try_kernel(BlasBackend, gemm_op const &, C &c, S const &alpha, A &a, B &b, S const &beta)
Pack logical host values and execute GEMM through value-preserving provider conversion.
Definition linalg/backends/blas/binary80.hpp:30
KernelAttempt
Result of one runtime kernel backend attempt.
Definition kernel_attempt.hpp:27
constexpr KernelAcceptance< KernelTypeAcceptance::no > kernel_types_no
Report that a backend rejects the argument types.
Definition dispatch_error.hpp:34
consteval auto kernel_accepts_types(BlasBackend, gemm_op const &, C &, S const &, A &, B &, S const &)
Accept host-readable native binary80 operands, including transforming accessors.
Definition linalg/backends/blas/binary80.hpp:19
constexpr KernelAcceptance< KernelTypeAcceptance::maybe > kernel_types_maybe
Report that runtime operand values determine backend acceptance.
Definition dispatch_error.hpp:37
Definition async/async.hpp:26
Backend-independent operation values used with normalized kernel operands.
std::size_t bytes
Definition presentation.cpp:18
std::size_t span
Definition presentation.cpp:2017
Backend value for generic CUDA runtime kernels and transfers.
Definition backend_selector.hpp:48
Accessor-respecting element copy operation tag.
Definition operation_tags.hpp:33
Concepts for tensor-level objects that resolve mdspan kernel operands.
#define CHECK(cond,...)
Definition trace.hpp:299
#define PANIC(...)
Definition trace.hpp:382
#define CHECK_EQUAL(a, b,...)
Definition trace.hpp:309