Skip to main content

kvbm_kernels/
tensor_kernels.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Safe-ish wrappers around the CUDA block/universal packing kernels.
5//!
6//! The core ideas:
7//! * A “block” represents the stack of `nl * no` tensors arranged either as NHD
8//!   (inner axes `[nt, nh, hd]`) or HND (inner axes `[nh, nt, hd]`).
9//! * A “universal” tensor is `[nh, nl, no, nt, hd]` stored contiguously.
10//! * An “operational” tensor is `[nl, no, inner]` with `inner = nt * nh * hd`.
11//!
12//! All pointer-list parameters (e.g. `universal_ptrs`, `src_ptrs`) must be
13//! device-accessible: allocated via `cudaMalloc` (device memory) or
14//! `cudaMallocHost` / `cuMemHostRegister` (pinned/registered/page-locked host memory).
15//!
16//! Host code calls these helpers with flattened pointer tables so a single
17//! launch can move many logical blocks in one go.
18
19#![allow(clippy::missing_safety_doc)]
20use std::ffi::c_void;
21
22use cudarc::runtime::sys::{cudaError_t, cudaStream_t};
23
24/// Numeric tags passed across the FFI boundary to select the CUDA template.
25#[cfg(feature = "permute_kernels")]
26#[repr(i32)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum TensorDataType {
29    F16 = 0,
30    BF16 = 1,
31    F32 = 2,
32    F64 = 3,
33}
34
35/// Identifies how each `[nt, nh, hd]` chunk is laid out in device memory.
36#[cfg(feature = "permute_kernels")]
37#[repr(i32)]
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum BlockLayout {
40    NHD = 0,
41    HND = 1,
42}
43
44#[cfg(feature = "permute_kernels")]
45#[allow(dead_code)]
46unsafe extern "C" {
47    fn kvbm_kernels_launch_universal_from_block(
48        universal_ptrs: *const *mut c_void,
49        block_ptrs: *const *const c_void,
50        num_blocks: usize,
51        nh: usize,
52        nl: usize,
53        no: usize,
54        nt: usize,
55        hd: usize,
56        dtype: i32,
57        layout: i32,
58        stream: cudaStream_t,
59    ) -> cudaError_t;
60
61    fn kvbm_kernels_launch_block_from_universal(
62        universal_ptrs: *const *const c_void,
63        block_ptrs: *const *mut c_void,
64        num_blocks: usize,
65        nh: usize,
66        nl: usize,
67        no: usize,
68        nt: usize,
69        hd: usize,
70        dtype: i32,
71        layout: i32,
72        stream: cudaStream_t,
73    ) -> cudaError_t;
74}
75
76/// Controls how `memcpy_batch` dispatches copies.
77#[repr(i32)]
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum MemcpyBatchMode {
80    /// Try cudaMemcpyBatchAsync, fall back to individual cudaMemcpyAsync on failure.
81    BatchedWithFallback = 0,
82    /// Only use individual cudaMemcpyAsync loop (never attempt batch API).
83    FallbackOnly = 1,
84    /// Try cudaMemcpyBatchAsync, return error on failure (no fallback).
85    BatchWithoutFallback = 2,
86}
87
88#[allow(dead_code)]
89unsafe extern "C" {
90    fn kvbm_kernels_launch_vectorized_copy(
91        src_ptrs: *mut *mut c_void,
92        dst_ptrs: *mut *mut c_void,
93        copy_size_bytes: usize,
94        num_pairs: i32,
95        stream: cudaStream_t,
96    ) -> cudaError_t;
97
98    fn kvbm_kernels_memcpy_batch(
99        src_ptrs: *const *const c_void,
100        dst_ptrs: *const *mut c_void,
101        size_per_copy: usize,
102        num_copies: usize,
103        mode: i32,
104        stream: cudaStream_t,
105    ) -> cudaError_t;
106
107    fn kvbm_kernels_has_memcpy_batch_async() -> bool;
108    fn kvbm_kernels_is_stub_build() -> bool;
109}
110
111/// Check if cudaMemcpyBatchAsync is available.
112///
113/// Returns true if the library was compiled with CUDA 12.9+ which provides
114/// the `cudaMemcpyBatchAsync` API for efficient batched memory transfers.
115pub fn is_memcpy_batch_available() -> bool {
116    unsafe { kvbm_kernels_has_memcpy_batch_async() }
117}
118
119/// Check if this library was built with stub kernels (no real CUDA).
120///
121/// Returns `true` if the library is using stubs that will abort on actual CUDA calls.
122/// Returns `false` if real CUDA kernels are available.
123///
124/// Downstream crates should use this to skip CUDA tests at runtime:
125/// ```ignore
126/// #[test]
127/// fn my_cuda_test() {
128///     if kvbm_kernels::is_using_stubs() {
129///         eprintln!("Skipping CUDA test: stub kernels in use");
130///         return;
131///     }
132///     // ... actual CUDA test code ...
133/// }
134/// ```
135pub fn is_using_stubs() -> bool {
136    unsafe { kvbm_kernels_is_stub_build() }
137}
138
139/// Batched memcpy using cudaMemcpyBatchAsync (CUDA 12.9+) and/or individual cudaMemcpyAsync.
140///
141/// Takes HOST arrays of src/dst pointers - no device allocation needed.
142/// Direction is auto-determined by CUDA from pointer types using cudaMemcpyDefault.
143///
144/// The `mode` parameter controls dispatch:
145/// - [`MemcpyBatchMode::BatchedWithFallback`]: try batch API, fall back to individual copies on error
146/// - [`MemcpyBatchMode::FallbackOnly`]: always use individual cudaMemcpyAsync loop
147/// - [`MemcpyBatchMode::BatchWithoutFallback`]: try batch API, return error if unavailable
148///
149/// # Safety
150/// - `src_ptrs` must point to a valid array of `num_copies` source pointers
151/// - `dst_ptrs` must point to a valid array of `num_copies` destination pointers
152/// - Each source/destination pointer pair must have at least `size_per_copy` bytes accessible
153/// - `stream` must be a valid CUDA stream handle
154pub unsafe fn memcpy_batch(
155    src_ptrs: *const *const c_void,
156    dst_ptrs: *const *mut c_void,
157    size_per_copy: usize,
158    num_copies: usize,
159    mode: MemcpyBatchMode,
160    stream: cudaStream_t,
161) -> cudaError_t {
162    unsafe {
163        kvbm_kernels_memcpy_batch(
164            src_ptrs,
165            dst_ptrs,
166            size_per_copy,
167            num_copies,
168            mode as i32,
169            stream,
170        )
171    }
172}
173
174/// Copy `num_blocks` stacks of NHD/HND tensors into universal form.
175///
176/// * `universal_ptrs` – device-accessible pointer to `num_blocks` universal bases.
177/// * `block_ptrs` – device-accessible pointer to a flattened `[num_blocks][nl*no]`
178///   table of chunk pointers.
179/// * `nh, nl, no, nt, hd` – logical dimensions of each universal tensor.
180/// * `stream` – CUDA stream used for the launch.
181#[cfg(feature = "permute_kernels")]
182#[allow(clippy::too_many_arguments)]
183pub unsafe fn universal_from_block(
184    universal_ptrs: *const *mut c_void,
185    block_ptrs: *const *const c_void,
186    num_blocks: usize,
187    nh: usize,
188    nl: usize,
189    no: usize,
190    nt: usize,
191    hd: usize,
192    dtype: TensorDataType,
193    layout: BlockLayout,
194    stream: cudaStream_t,
195) -> cudaError_t {
196    unsafe {
197        kvbm_kernels_launch_universal_from_block(
198            universal_ptrs,
199            block_ptrs,
200            num_blocks,
201            nh,
202            nl,
203            no,
204            nt,
205            hd,
206            dtype as i32,
207            layout as i32,
208            stream,
209        )
210    }
211}
212
213/// Copy `num_blocks` universal tensors back into their block stacks.
214#[cfg(feature = "permute_kernels")]
215#[allow(clippy::too_many_arguments)]
216pub unsafe fn block_from_universal(
217    universal_ptrs: *const *const c_void,
218    block_ptrs: *const *mut c_void,
219    num_blocks: usize,
220    nh: usize,
221    nl: usize,
222    no: usize,
223    nt: usize,
224    hd: usize,
225    dtype: TensorDataType,
226    layout: BlockLayout,
227    stream: cudaStream_t,
228) -> cudaError_t {
229    unsafe {
230        kvbm_kernels_launch_block_from_universal(
231            universal_ptrs,
232            block_ptrs,
233            num_blocks,
234            nh,
235            nl,
236            no,
237            nt,
238            hd,
239            dtype as i32,
240            layout as i32,
241            stream,
242        )
243    }
244}
245
246/// Launch vectorized copy between arbitrary device-visible pointer pairs.
247///
248/// This kernel automatically selects optimal vectorization (4/8/16 bytes) based on
249/// pointer alignment. It is useful for copying between non-contiguous memory regions
250/// where each pair has the same copy size.
251///
252/// Both source and destination pointers may refer to any device-visible memory,
253/// including device allocations (`cudaMalloc`) and pinned host memory
254/// (`cudaMallocHost` / `cudaHostAlloc`). CUDA unified addressing resolves the
255/// actual location at runtime.
256///
257/// # Arguments
258/// * `src_ptrs` - Device-accessible pointer to array of source pointers (each pointing to device-visible memory)
259/// * `dst_ptrs` - Device-accessible pointer to array of destination pointers (each pointing to device-visible memory)
260/// * `copy_size_bytes` - Size of each copy in bytes (same for all pairs)
261/// * `num_pairs` - Number of pointer pairs to copy
262/// * `stream` - CUDA stream for async execution
263///
264/// # Safety
265/// - All pointers in the src/dst arrays must be valid device-visible pointers
266///   (device memory or pinned host memory)
267/// - Each pointer must have at least `copy_size_bytes` bytes accessible
268/// - The pointer arrays themselves must be in device memory with at least `num_pairs` entries
269/// - `stream` must be a valid CUDA stream handle
270pub unsafe fn vectorized_copy(
271    src_ptrs: *mut *mut c_void,
272    dst_ptrs: *mut *mut c_void,
273    copy_size_bytes: usize,
274    num_pairs: i32,
275    stream: cudaStream_t,
276) -> cudaError_t {
277    unsafe {
278        kvbm_kernels_launch_vectorized_copy(src_ptrs, dst_ptrs, copy_size_bytes, num_pairs, stream)
279    }
280}
281
282// Tests are gated to only run when:
283// 1. testing-cuda feature is enabled
284// 2. permute_kernels feature is enabled (tests use universal kernels)
285// 3. NOT using stub kernels (stub_kernels cfg is set by build.rs when no nvcc)
286#[cfg(all(
287    test,
288    feature = "testing-cuda",
289    feature = "permute_kernels",
290    not(stub_kernels)
291))]
292mod tests {
293    use super::*;
294    use cudarc::driver::result::memset_d8_async;
295    use cudarc::driver::{CudaContext, CudaSlice, DevicePtr, DevicePtrMut, DriverError};
296    use cudarc::runtime::sys as cuda_runtime;
297
298    #[test]
299    fn universal_roundtrip() -> Result<(), DriverError> {
300        let device_count = match CudaContext::device_count() {
301            Ok(count) => count,
302            Err(_) => return Ok(()),
303        };
304        if device_count <= 0 {
305            return Ok(());
306        }
307
308        let ctx = CudaContext::new(0)?;
309        let stream = ctx.default_stream();
310        let stream_raw = stream.cu_stream() as cuda_runtime::cudaStream_t;
311
312        let nh = 2usize;
313        let nl = 2usize;
314        let no = 2usize;
315        let nt = 3usize;
316        let hd = 4usize;
317        let inner = nt * nh * hd;
318        let chunk_count = nl * no;
319        let block_volume = nh * nl * no * nt * hd;
320        let num_blocks = 2usize;
321
322        let dtype = TensorDataType::F32;
323        let layout = BlockLayout::NHD;
324
325        let mut host_block_chunks: Vec<Vec<Vec<f32>>> = Vec::with_capacity(num_blocks);
326        let mut block_slices: Vec<Vec<CudaSlice<f32>>> = Vec::with_capacity(num_blocks);
327        let mut block_ptr_values: Vec<usize> = Vec::with_capacity(num_blocks * chunk_count);
328
329        for block_idx in 0..num_blocks {
330            let mut host_chunks_for_block = Vec::with_capacity(chunk_count);
331            let mut slices_for_block = Vec::with_capacity(chunk_count);
332            for chunk_idx in 0..chunk_count {
333                let global_idx = block_idx * chunk_count + chunk_idx;
334                let mut host_chunk = Vec::with_capacity(inner);
335                for offset in 0..inner {
336                    host_chunk.push((global_idx * inner + offset) as f32 + 0.25f32);
337                }
338                let slice = stream.clone_htod(&host_chunk)?;
339                {
340                    let (ptr_raw, _guard) = slice.device_ptr(&stream);
341                    block_ptr_values.push(ptr_raw as usize);
342                }
343                slices_for_block.push(slice);
344                host_chunks_for_block.push(host_chunk);
345            }
346            block_slices.push(slices_for_block);
347            host_block_chunks.push(host_chunks_for_block);
348        }
349
350        let block_ptrs = stream.clone_htod(block_ptr_values.as_slice())?;
351
352        let mut universal_slices = Vec::with_capacity(num_blocks);
353        let mut universal_ptr_values = Vec::with_capacity(num_blocks);
354        for _ in 0..num_blocks {
355            let mut slice = unsafe { stream.alloc::<f32>(block_volume)? };
356            {
357                let (ptr_raw, _guard) = slice.device_ptr_mut(&stream);
358                universal_ptr_values.push(ptr_raw as usize);
359                unsafe {
360                    memset_d8_async(
361                        ptr_raw,
362                        0xDE,
363                        block_volume * std::mem::size_of::<f32>(),
364                        stream.cu_stream(),
365                    )?;
366                }
367            }
368            universal_slices.push(slice);
369        }
370        let universal_ptrs = stream.clone_htod(universal_ptr_values.as_slice())?;
371
372        // Block -> Universal
373        {
374            let (block_ptrs_raw, _block_guard) = block_ptrs.device_ptr(&stream);
375            let block_ptrs_ptr = block_ptrs_raw as usize as *const *const c_void;
376            let (universal_ptrs_raw, _univ_guard) = universal_ptrs.device_ptr(&stream);
377            let universal_ptrs_ptr = universal_ptrs_raw as usize as *const *mut c_void;
378
379            let status = unsafe {
380                super::universal_from_block(
381                    universal_ptrs_ptr,
382                    block_ptrs_ptr,
383                    num_blocks,
384                    nh,
385                    nl,
386                    no,
387                    nt,
388                    hd,
389                    dtype,
390                    layout,
391                    stream_raw,
392                )
393            };
394            assert_eq!(status, cuda_runtime::cudaError::cudaSuccess);
395        }
396        stream.synchronize()?;
397
398        let inner_offset = |nt_idx: usize, nh_idx: usize, hd_idx: usize| match layout {
399            BlockLayout::NHD => ((nt_idx * nh) + nh_idx) * hd + hd_idx,
400            BlockLayout::HND => ((nh_idx * nt) + nt_idx) * hd + hd_idx,
401        };
402
403        for (block_idx, universal_slice) in universal_slices.iter().enumerate().take(num_blocks) {
404            let host_universal = stream.clone_dtoh(universal_slice)?;
405            for nh_idx in 0..nh {
406                for nl_idx in 0..nl {
407                    for no_idx in 0..no {
408                        for nt_idx in 0..nt {
409                            for hd_idx in 0..hd {
410                                let universal_index =
411                                    ((((nh_idx * nl + nl_idx) * no + no_idx) * nt + nt_idx) * hd)
412                                        + hd_idx;
413                                let chunk_idx = nl_idx * no + no_idx;
414                                let offset = inner_offset(nt_idx, nh_idx, hd_idx);
415                                let expected = ((block_idx * chunk_count + chunk_idx) * inner
416                                    + offset) as f32
417                                    + 0.25f32;
418                                let value = host_universal[universal_index];
419                                assert!(
420                                    (value - expected).abs() < 1e-5,
421                                    "universal mismatch block {} [{} {} {} {} {}]: {} vs {}",
422                                    block_idx,
423                                    nh_idx,
424                                    nl_idx,
425                                    no_idx,
426                                    nt_idx,
427                                    hd_idx,
428                                    value,
429                                    expected
430                                );
431                            }
432                        }
433                    }
434                }
435            }
436        }
437
438        // Universal -> Block (poison-fill destination before reverse pass)
439        for block in &mut block_slices {
440            for slice in block {
441                let (dptr, _guard) = slice.device_ptr_mut(&stream);
442                unsafe {
443                    memset_d8_async(
444                        dptr,
445                        0xDE,
446                        inner * std::mem::size_of::<f32>(),
447                        stream.cu_stream(),
448                    )?;
449                }
450            }
451        }
452        stream.synchronize()?;
453
454        {
455            let (block_ptrs_raw, _block_guard) = block_ptrs.device_ptr(&stream);
456            let block_ptrs_mut = block_ptrs_raw as usize as *const *mut c_void;
457            let (universal_ptrs_raw, _univ_guard) = universal_ptrs.device_ptr(&stream);
458            let universal_ptrs_const = universal_ptrs_raw as usize as *const *const c_void;
459            let status = unsafe {
460                super::block_from_universal(
461                    universal_ptrs_const,
462                    block_ptrs_mut,
463                    num_blocks,
464                    nh,
465                    nl,
466                    no,
467                    nt,
468                    hd,
469                    dtype,
470                    layout,
471                    stream_raw,
472                )
473            };
474            assert_eq!(status, cuda_runtime::cudaError::cudaSuccess);
475        }
476        stream.synchronize()?;
477
478        for block_idx in 0..num_blocks {
479            for chunk_idx in 0..chunk_count {
480                let host_chunk = stream.clone_dtoh(&block_slices[block_idx][chunk_idx])?;
481                for (inner_idx, value) in host_chunk.iter().enumerate() {
482                    let expected = host_block_chunks[block_idx][chunk_idx][inner_idx];
483                    assert!(
484                        (value - expected).abs() < 1e-5,
485                        "block mismatch block {} chunk {} offset {}: {} vs {}",
486                        block_idx,
487                        chunk_idx,
488                        inner_idx,
489                        value,
490                        expected
491                    );
492                }
493            }
494        }
495
496        Ok(())
497    }
498
499    /// Test the vectorized copy kernel directly with aligned data.
500    #[test]
501    fn test_vectorized_copy_aligned() -> Result<(), DriverError> {
502        let device_count = match CudaContext::device_count() {
503            Ok(count) => count,
504            Err(_) => return Ok(()),
505        };
506        if device_count <= 0 {
507            return Ok(());
508        }
509
510        let ctx = CudaContext::new(0)?;
511        let stream = ctx.default_stream();
512        let stream_raw = stream.cu_stream() as cuda_runtime::cudaStream_t;
513
514        // Create test data - 8-byte aligned for vectorized copy
515        let num_pairs = 4;
516        let copy_size = 256usize; // 256 bytes, divisible by 16 for int4 vectorization
517
518        // Source data
519        let mut src_slices = Vec::with_capacity(num_pairs);
520        let mut src_ptr_values = Vec::with_capacity(num_pairs);
521        let mut expected_data = Vec::with_capacity(num_pairs);
522
523        for i in 0..num_pairs {
524            let data: Vec<u8> = (0..copy_size)
525                .map(|j| ((i * copy_size + j) % 256) as u8)
526                .collect();
527            expected_data.push(data.clone());
528            let slice = stream.clone_htod(&data)?;
529            {
530                let (ptr, _guard) = slice.device_ptr(&stream);
531                src_ptr_values.push(ptr as usize);
532            }
533            src_slices.push(slice);
534        }
535
536        // Destination buffers
537        let mut dst_slices = Vec::with_capacity(num_pairs);
538        let mut dst_ptr_values = Vec::with_capacity(num_pairs);
539
540        for _ in 0..num_pairs {
541            let mut slice = unsafe { stream.alloc::<u8>(copy_size)? };
542            {
543                let (ptr, _guard) = slice.device_ptr_mut(&stream);
544                dst_ptr_values.push(ptr as usize);
545            }
546            dst_slices.push(slice);
547        }
548
549        // Upload pointer arrays to device
550        let src_ptrs = stream.clone_htod(&src_ptr_values)?;
551        let dst_ptrs = stream.clone_htod(&dst_ptr_values)?;
552
553        // Launch vectorized copy
554        {
555            let (src_ptrs_raw, _src_guard) = src_ptrs.device_ptr(&stream);
556            let (dst_ptrs_raw, _dst_guard) = dst_ptrs.device_ptr(&stream);
557
558            let status = unsafe {
559                super::vectorized_copy(
560                    src_ptrs_raw as usize as *mut *mut c_void,
561                    dst_ptrs_raw as usize as *mut *mut c_void,
562                    copy_size,
563                    num_pairs as i32,
564                    stream_raw,
565                )
566            };
567            assert_eq!(status, cuda_runtime::cudaError::cudaSuccess);
568        }
569        stream.synchronize()?;
570
571        // Verify results
572        for i in 0..num_pairs {
573            let result = stream.clone_dtoh(&dst_slices[i])?;
574            assert_eq!(result, expected_data[i], "Mismatch at pair {}", i);
575        }
576
577        Ok(())
578    }
579}