工作小记-pytorch的算子优化: 转gpu核函数 python调c

记录一下关于torch的算子优化。主要是根据torch的torch.utils.cpp_extension 去把一些torch上的python变换放到gpu中去并行执行,写好对应的测试用例保证c和python的结果一致。

https://github.com/PrincetonLIPS/numpy-hilbert-curve 中有python版本的希尔伯特编码的实现。目前是期望提速该函数

1 编写cuda核函数

这里的希尔伯特编码是批量进行的,正好对应cuda中的线程,每个线程就可以对一个点进行编码

编写c函数供外部调用,外加pybind进行封装提供python api 供python调用

hilbert_cuda.cu

cpp 复制代码
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>

__global__ void hilbert_encode_kernel(const uint8_t* locs_uint8, int num_dims, int num_bits, int64_t* output, int N) 
{
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid >= N) return;
    output[tid] = 0;
    __shared__ bool s_gray[256][64];
    bool* g = s_gray[threadIdx.x];

    const uint8_t* src = locs_uint8 + tid * num_dims * 8;

    // 1. 位提取
    uint8_t bitpack_mask_rev[8] = {128, 64, 32, 16, 8, 4, 2, 1};

    for (int dim = 0; dim < num_dims; dim++) {
        // 与 python locs_uint8.flip(-1) 对齐,翻转 8 个字节
        uint8_t flipped_bytes[8];
        #pragma unroll
        for (int i = 0; i < 8; i++) {
            // 翻转顺序读取
            flipped_bytes[i] = src[dim * 8 + (7 - i)];
        }

        // 依次从高位到低位提取 8*8=64 bits
        bool bits_64[64];
        int idx = 0;
        #pragma unroll
        for (int i = 0; i < 8; i++) {
            uint8_t val = flipped_bytes[i];
            #pragma unroll
            for (int b = 0; b < 8; b++) {
                bits_64[idx++] = (val & bitpack_mask_rev[b]) != 0;
            }
        }

        // 与 Python 保持一致,只取最后 num_bits 位
        int start_idx = 64 - num_bits;
        for (int b = 0; b < num_bits; b++) {
            g[dim * num_bits + b] = bits_64[start_idx + b];
        }
    }

    // 2. Skilling变换
    for (int bit = 0; bit < num_bits; bit++) {
        for (int dim = 0; dim < num_dims; dim++) {
            if (bit + 1 >= num_bits) continue;
            bool mask = g[dim * num_bits + bit];
            
            // 修改整个bit+1到最后的区间
            for (int b = bit + 1; b < num_bits; b++) {
                bool orig_0 = g[0 * num_bits + b];
                bool orig_dim = g[dim * num_bits + b];
                g[0 * num_bits + b] ^= mask;
                bool to_flip = (!mask) & (g[0 * num_bits + b] ^ orig_dim);
                g[dim * num_bits + b] ^= to_flip;
                g[0 * num_bits + b] ^= to_flip;
            }
        }
    }

    // 3. Gray转Binary
    // 先调整位序为bit-first格式
    bool temp[64];  // 临时数组存储转换后的数据
    for (int b = 0; b < num_bits; b++) {
        for (int dim = 0; dim < num_dims; dim++) {
            temp[b * num_dims + dim] = g[dim * num_bits + b];
        }
    }
    
    // 2. 复制回原数组
    for (int i = 0; i < num_bits * num_dims; i++) {
        g[i] = temp[i];
    }

    // 3. Gray to Binary转换
    for (int i = 1; i < num_bits * num_dims; i++) {
        g[i] = g[i] ^ g[i - 1];
    }

    // 4.1 模拟padding操作
    const int total_bits = 64;
    const int extra_bits = total_bits - num_bits * num_dims;
    bool padded[64] = {0};  // 初始化为0
    // 复制原始数据到padded的后面部分
    for(int i = 0; i < num_bits * num_dims; i++) {
        padded[extra_bits + i] = g[i];
    }

    // 4.2 模拟flip操作
    bool flipped[64];
    #pragma unroll
    for(int i = 0; i < 64; i++) {
        flipped[i] = padded[63 - i];
    }

    // 4.3 位打包
    int64_t result = 0;
    #pragma unroll
    for(int i = 0; i < 64; i++) {
        if(flipped[i]) {
            int64_t bit_value = 1ULL << i;
            result |= bit_value;
        }
    }

    output[tid] = result;
}

// 示例 CUDA 内核签名
extern "C" void hilbert_encode_cuda_optimized(torch::Tensor locs_uint8, int num_dims, int num_bits, torch::Tensor output) {
    // 获取指针
    auto locs_ptr = locs_uint8.data_ptr<uint8_t>();
    auto output_ptr = output.data_ptr<int64_t>();
    
    // CUDA核函数调用
    int N = locs_uint8.size(0);
    dim3 blocks((N + 255) / 256);
    dim3 threads(256);
    hilbert_encode_kernel<<<blocks, threads>>>(locs_ptr, num_dims, num_bits, output_ptr, N);
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
    m.def("hilbert_encode_cuda_optimized",
          &hilbert_encode_cuda_optimized,
          "Optimized Hilbert Encode CUDA");
}

2.1 load形式调用

torch.utils.cpp_extension 里的load会运行时根据输入文件,编译这个so库,记得环境变量里要设置好对应的cpp版本等

cpp 复制代码
# 2) 编译并加载上面的CUDA扩展
cuda_path = os.path.join(os.path.dirname(__file__), "hilbert_cuda.cu")

hilbert_cuda = load(
    name="hilbert_cuda",
    sources=[cuda_path],
    verbose=True
)

def encode_cuda_optimized(locs, num_dims, num_bits):
    device = locs.device
    N = locs.shape[0]
    
    # 格式化输入
    locs_uint8 = locs.contiguous().view(torch.uint8).reshape(N, num_dims, 8)
    
    # 分配输出 
    output = torch.empty(N, dtype=torch.int64, device=device)
    
    # 执行kernel,并传递output
    with torch.cuda.device(device):
        hilbert_cuda.hilbert_encode_cuda_optimized(locs_uint8, num_dims, num_bits, output)
        torch.cuda.synchronize()
    
    return output

test.py

python 复制代码
"""
Hilbert Order
Modified from https://github.com/PrincetonLIPS/numpy-hilbert-curve

Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Kaixin Xu
Please cite our work if the code is helpful to you.
"""

import torch
from torch.utils.cpp_extension import load
import os
import time

# 2) 编译并加载上面的CUDA扩展
cuda_path = os.path.join(os.path.dirname(__file__), "hilbert_cuda.cu")

hilbert_cuda = load(
    name="hilbert_cuda",
    sources=[cuda_path],
    verbose=True
)

def encode_cuda_optimized(locs, num_dims, num_bits):
    device = locs.device
    N = locs.shape[0]
    
    # 格式化输入
    locs_uint8 = locs.contiguous().view(torch.uint8).reshape(N, num_dims, 8)
    
    # 分配输出 
    output = torch.empty(N, dtype=torch.int64, device=device)
    
    # 执行kernel,并传递output
    with torch.cuda.device(device):
        hilbert_cuda.hilbert_encode_cuda_optimized(locs_uint8, num_dims, num_bits, output)
        torch.cuda.synchronize()
    
    return output

def right_shift(binary, k=1, axis=-1):
    """Right shift an array of binary values.

    Parameters:
    -----------
     binary: An ndarray of binary values.

     k: The number of bits to shift. Default 1.

     axis: The axis along which to shift.  Default -1.

    Returns:
    --------
     Returns an ndarray with zero prepended and the ends truncated, along
     whatever axis was specified."""

    # If we're shifting the whole thing, just return zeros.
    if binary.shape[axis] <= k:
        return torch.zeros_like(binary)

    # Determine the padding pattern.
    # padding = [(0,0)] * len(binary.shape)
    # padding[axis] = (k,0)

    # Determine the slicing pattern to eliminate just the last one.
    slicing = [slice(None)] * len(binary.shape)
    slicing[axis] = slice(None, -k)
    shifted = torch.nn.functional.pad(
        binary[tuple(slicing)], (k, 0), mode="constant", value=0
    )

    return shifted

def gray2binary_old(gray, axis=-1):
    """Convert an array of Gray codes back into binary values.

    Parameters:
    -----------
     gray: An ndarray of gray codes.

     axis: The axis along which to perform Gray decoding. Default=-1.

    Returns:
    --------
     Returns an ndarray of binary values.
    """

    # Loop the log2(bits) number of times necessary, with shift and xor.
    shift = 2 ** (torch.Tensor([gray.shape[axis]]).log2().ceil().int() - 1)
    while shift > 0:
        gray = torch.logical_xor(gray, right_shift(gray, shift))
        shift = torch.div(shift, 2, rounding_mode="floor")
    return gray


def encode(locs, num_dims, num_bits):
    """Decode an array of locations in a hypercube into a Hilbert integer.

    This is a vectorized-ish version of the Hilbert curve implementation by John
    Skilling as described in:

    Skilling, J. (2004, April). Programming the Hilbert curve. In AIP Conference
      Proceedings (Vol. 707, No. 1, pp. 381-387). American Institute of Physics.

    Params:
    -------
     locs - An ndarray of locations in a hypercube of num_dims dimensions, in
            which each dimension runs from 0 to 2**num_bits-1.  The shape can
            be arbitrary, as long as the last dimension of the same has size
            num_dims.

     num_dims - The dimensionality of the hypercube. Integer.

     num_bits - The number of bits for each dimension. Integer.

    Returns:
    --------
     The output is an ndarray of uint64 integers with the same shape as the
     input, excluding the last dimension, which needs to be num_dims.
    """

    # Keep around the original shape for later.
    orig_shape = locs.shape
    bitpack_mask = 1 << torch.arange(0, 8).to(locs.device)
    bitpack_mask_rev = bitpack_mask.flip(-1)

    if orig_shape[-1] != num_dims:
        raise ValueError(
            """
      The shape of locs was surprising in that the last dimension was of size
      %d, but num_dims=%d.  These need to be equal.
      """
            % (orig_shape[-1], num_dims)
        )

    if num_dims * num_bits > 63:
        raise ValueError(
            """
      num_dims=%d and num_bits=%d for %d bits total, which can't be encoded
      into a int64.  Are you sure you need that many points on your Hilbert
      curve?
      """
            % (num_dims, num_bits, num_dims * num_bits)
        )

    # Treat the location integers as 64-bit unsigned and then split them up into
    # a sequence of uint8s.  Preserve the association by dimension.
    locs_uint8 = locs.long().view(torch.uint8).reshape((-1, num_dims, 8)).flip(-1)

    # Now turn these into bits and truncate to num_bits.
    gray = (
        locs_uint8.unsqueeze(-1)
        .bitwise_and(bitpack_mask_rev)
        .ne(0)
        .byte()
        .flatten(-2, -1)[..., -num_bits:]
    )

    # Run the decoding process the other way.
    # Iterate forwards through the bits.
    for bit in range(0, num_bits):
        # Iterate forwards through the dimensions.
        for dim in range(0, num_dims):
            # Identify which ones have this bit active.
            mask = gray[:, dim, bit]

            # Where this bit is on, invert the 0 dimension for lower bits.
            gray[:, 0, bit + 1 :] = torch.logical_xor(
                gray[:, 0, bit + 1 :], mask[:, None]
            )

            # Where the bit is off, exchange the lower bits with the 0 dimension.
            to_flip = torch.logical_and(
                torch.logical_not(mask[:, None]).repeat(1, gray.shape[2] - bit - 1),
                torch.logical_xor(gray[:, 0, bit + 1 :], gray[:, dim, bit + 1 :]),
            )
            gray[:, dim, bit + 1 :] = torch.logical_xor(
                gray[:, dim, bit + 1 :], to_flip
            )
            gray[:, 0, bit + 1 :] = torch.logical_xor(gray[:, 0, bit + 1 :], to_flip)

    # Now flatten out.
    gray = gray.swapaxes(1, 2).reshape((-1, num_bits * num_dims))

    # Convert Gray back to binary.
    hh_bin = gray2binary_old(gray)

    # Pad back out to 64 bits.
    extra_dims = 64 - num_bits * num_dims
    padded = torch.nn.functional.pad(hh_bin, (extra_dims, 0), "constant", 0)

    # Convert binary values into uint8s.
    hh_uint8 = (
        (padded.flip(-1).reshape((-1, 8, 8)) * bitpack_mask)
        .sum(2)
        .squeeze()
        .type(torch.uint8)
    )

    # Convert uint8s into uint64s.
    hh_uint64 = hh_uint8.view(torch.int64).squeeze()

    return hh_uint64

def encode_optimized(locs, num_dims, num_bits):
    """
    与 encode 数值等价、性能更优的 Hilbert 曲线编码,支持 GPU 并行。
    """
    shp = locs.shape
    if num_dims * num_bits > 63:
        raise ValueError("Cannot encode more than 63 bits in total.")
    device = locs.device

    # print("\n=== Python Version Debug ===")
    # print(f"Input shape: {locs.shape}")
    # print(f"First sample raw data:")
    # for dim in range(num_dims):
    #     print(f"dim {dim}: {locs[1,dim]:08b} ({locs[1,dim]})")  # 添加原始二进制表示

    # 与原版一致的步骤:先按字节拆分并翻转
    locs_uint8 = locs.view(torch.uint8).reshape(-1, num_dims, 8).flip(-1)
    # print("\n=== Bit Extraction Process ===")
    # print("1. After reshape & flip:")
    # print(f"locs_uint8[0]:\n{locs_uint8[1]}")
    bitpack_mask = 1 << torch.arange(0, 8, device=device)
    bitpack_mask_rev = bitpack_mask.flip(-1)
    # print(f"bitpack_mask_rev: {bitpack_mask_rev}")

    # 提取高位到低位(与原 encode 的做法一致)
    gray = (
        locs_uint8.unsqueeze(-1)
        .bitwise_and(bitpack_mask_rev)
        .ne(0)
        .byte()
        .flatten(-2, -1)[..., -num_bits:]
        .reshape(-1, num_dims, num_bits)
    )
    # print("\n=== After bit extraction ===")
    # print(f"First sample bits by dimension:")
    # for dim in range(num_dims):
    #     bits = ""
    #     for b in range(num_bits):
    #         bits += str(int(gray[1,dim,b]))
    #     print(f"dim {dim}: {bits}")

    # Skilling 旋转/翻转
    for bit in range(num_bits):
        for dim in range(num_dims):
            mask = gray[:, dim, bit]
            if bit + 1 < num_bits:
                gray[:, 0, bit+1:] ^= mask.unsqueeze(-1)
                to_flip = (~mask.unsqueeze(-1)) & (gray[:, 0, bit+1:] ^ gray[:, dim, bit+1:])
                gray[:, dim, bit+1:] ^= to_flip
                gray[:, 0, bit+1:] ^= to_flip
    # print("\n=== After Skilling Transform ===")
    # print(f"First sample bits after Skilling:")
    # for dim in range(num_dims):
    #     bits = ""
    #     for b in range(num_bits):
    #         bits += str(int(gray[1,dim,b]))
    #     print(f"dim {dim}: {bits}")

    # Gray→binary
    gview = gray.swapaxes(1, 2).reshape(-1, num_bits * num_dims)
    # print("\n=== After swapaxes ===")
    # print(f"Bit order after swapaxes:")
    # print(gview[1])
    binary = torch.cumsum(gview, dim=-1) % 2
    # print("\n=== After Gray to Binary ===")
    # print(f"First sample binary representation:")
    # print(f"binary[0]: {binary[1].tolist()}")
    binary = binary.long()  # 转换为整数类型

    # 填充到 64 位并打包
    extra_bits = 64 - num_bits * num_dims
    padded = torch.nn.functional.pad(binary, (extra_bits, 0)).long()

    # 使用整数类型的 bitmask
    bitmask = 1 << torch.arange(64, device=device).long()
    hilbert_indices = (padded.flip(-1) * bitmask).sum(dim=-1).long()
    # print("\n=== Final Result ===")
    # print(f"Final packed result: {hilbert_indices[1]}")

    return hilbert_indices.reshape(shp[:-1])

# 测试用例
torch.manual_seed(41) #可修改

# 假设我们有 3 个维度
num_dims = 3  # 3维
import random

npoints = 10
total_time_encode = 0
total_time_encode_new = 0
num_runs = 10 # 可根据需要调整测试次数

for i in range(0, num_runs):
    num_bits = random.randint(3, 20)  
    #num_bits = 3
    torch.manual_seed(i)
    locs1 = torch.randint(0, 2**num_bits, (npoints, num_dims), dtype=torch.int64)
    locs2 = locs1.to('cuda')
    num_bits = depth = int(torch.max(locs1)).bit_length()
    print(num_bits)
    # 测试 encode
    start_time = time.time()
    encoded_values_encode = encode_optimized(locs1, num_dims, num_bits)
    end_time = time.time()
    total_time_encode += (end_time - start_time)

    # 测试 encode_new
    torch.cuda.synchronize()
    start_time = time.time()
    encoded_values_encode_new = encode_cuda_optimized(locs2, num_dims, num_bits)
    end_time = time.time()
    total_time_encode_new += (end_time - start_time)

    encoded_values_encode_new_cpu = encoded_values_encode_new.cpu()
    # 比较 encode 和 encode_new 的结果
    if torch.equal(encoded_values_encode, encoded_values_encode_new_cpu):
        print("encode 和 encode_new 生成的编码结果完全相同。")
    else:
        print("encode 和 encode_new 生成的编码结果存在差异。")
        # 计算不匹配的数量
        mismatches = (encoded_values_encode != encoded_values_encode_new_cpu).sum().item()
        print(f"不匹配的元素数量: {mismatches}")

        if mismatches > 0:
            mismatched_indices = (encoded_values_encode != encoded_values_encode_new_cpu).nonzero(as_tuple=True)[0]

            num_to_print = min(10, mismatches)
            print(f"前 {num_to_print} 个不匹配的示例:")
            for i in mismatched_indices[:num_to_print]:
                print(f"索引 {i.item()}: encode = {encoded_values_encode[i].item()}, encode_new = {encoded_values_encode_new[i].item()}")

#分别打印一下encode以及encode_optimized测试耗时
print("encode 耗时: ", total_time_encode/num_runs)
print("encode_optimized 耗时: ", total_time_encode_new/num_runs)

# Encoded Values:
# tensor([12502877, 15055001, 12711671,  ..., 10978447,  5159808, 12237478])

2.2 import形式调用

setup.py

cpp 复制代码
from setuptools import setup
from torch.utils.cpp_extension import BuildExtension, CUDAExtension
import os

setup(
    name='hilbert_cuda',  # 扩展模块的名称
    ext_modules=[
        CUDAExtension(
            name='hilbert_cuda',  # 与 Python 中加载的模块名一致
            sources=['hilbert_cuda.cu'],  # 源文件列表
            extra_compile_args={
                'cxx': ['-O3'],  # C++ 编译器优化选项
                'nvcc': [
                    '-O3',  # CUDA 编译器优化选项
                    '--use_fast_math',  # 使用快速数学库
                    '-gencode=arch=compute_89,code=sm_89',  # 针对 RTX 4090 的架构
                    '-std=c++17',  # 使用 C++17 标准
                    '--expt-relaxed-constexpr',  # 允许更宽松的常量表达式
                ],
            },
            include_dirs=[],  # 如有需要,可添加额外的包含目录
        ),
    ],
    cmdclass={
        'build_ext': BuildExtension  # 指定构建扩展的命令类
    }
)

build

执行完毕后对应目录下会生成 对应的hilbert_cuda.cpython-38-x86_64-linux-gnu.so,python调用只看名字,是c的so也没问题

bash 复制代码
[root@218c3c31e907 6]# python3 setup.py build
running build
running build_ext
/usr/local/lib/python3.8/site-packages/torch/utils/cpp_extension.py:398: UserWarning: There are no /usr/local/gcc-11.4/bin/g++ version bounds defined for CUDA version 11.8
  warnings.warn(f'There are no {compiler_name} version bounds defined for CUDA version {cuda_str_version}')
building 'hilbert_cuda' extension
Emitting ninja build file /zyx/6/build/temp.linux-x86_64-3.8/build.ninja...
Compiling objects...
Allowing ninja to set a default number of workers... (overridable by setting the environment variable MAX_JOBS=N)
ninja: no work to do.
/usr/local/gcc-11.4/bin/g++ -pthread -shared /zyx/6/build/temp.linux-x86_64-3.8/hilbert_cuda.o -L/usr/local/lib/python3.8/site-packages/torch/lib -L/usr/local/cuda/lib64 -lc10 -ltorch -ltorch_cpu -ltorch_python -lcudart -lc10_cuda -ltorch_cuda -o build/lib.linux-x86_64-3.8/hilbert_cuda.cpython-38-x86_64-linux-gnu.so

此时在对应有so的目录下就不会报错了

python 复制代码
import torch
import hilbert_cuda 

install

执行完毕后会把包安装到对应的python的包路径下,就可以直接调用了

bash 复制代码
[root@218c3c31e907 hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg]# pip show hilbert_cuda
Name: hilbert-cuda
Version: 0.0.0
Summary: UNKNOWN
Home-page: UNKNOWN
Author: 
Author-email: 
License: UNKNOWN
Location: /usr/local/lib/python3.8/site-packages/hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg
Requires: 
Required-by:

python3 setup.py install

bash 复制代码
[root@218c3c31e907 6]# python3 setup.py install
running install
running bdist_egg
running egg_info
creating hilbert_cuda.egg-info
writing hilbert_cuda.egg-info/PKG-INFO
writing dependency_links to hilbert_cuda.egg-info/dependency_links.txt
writing top-level names to hilbert_cuda.egg-info/top_level.txt
writing manifest file 'hilbert_cuda.egg-info/SOURCES.txt'
listing git files failed - pretending there aren't any
reading manifest file 'hilbert_cuda.egg-info/SOURCES.txt'
writing manifest file 'hilbert_cuda.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-x86_64/egg
running install_lib
running build_ext
/usr/local/lib/python3.8/site-packages/torch/utils/cpp_extension.py:398: UserWarning: There are no /usr/local/gcc-11.4/bin/g++ version bounds defined for CUDA version 11.8
  warnings.warn(f'There are no {compiler_name} version bounds defined for CUDA version {cuda_str_version}')
building 'hilbert_cuda' extension
Emitting ninja build file /zyx/6/build/temp.linux-x86_64-3.8/build.ninja...
Compiling objects...
Allowing ninja to set a default number of workers... (overridable by setting the environment variable MAX_JOBS=N)
ninja: no work to do.
/usr/local/gcc-11.4/bin/g++ -pthread -shared /zyx/6/build/temp.linux-x86_64-3.8/hilbert_cuda.o -L/usr/local/lib/python3.8/site-packages/torch/lib -L/usr/local/cuda/lib64 -lc10 -ltorch -ltorch_cpu -ltorch_python -lcudart -lc10_cuda -ltorch_cuda -o build/lib.linux-x86_64-3.8/hilbert_cuda.cpython-38-x86_64-linux-gnu.so
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/egg
copying build/lib.linux-x86_64-3.8/hilbert_cuda.cpython-38-x86_64-linux-gnu.so -> build/bdist.linux-x86_64/egg
creating stub loader for hilbert_cuda.cpython-38-x86_64-linux-gnu.so
byte-compiling build/bdist.linux-x86_64/egg/hilbert_cuda.py to hilbert_cuda.cpython-38.pyc
creating build/bdist.linux-x86_64/egg/EGG-INFO
copying hilbert_cuda.egg-info/PKG-INFO -> build/bdist.linux-x86_64/egg/EGG-INFO
copying hilbert_cuda.egg-info/SOURCES.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying hilbert_cuda.egg-info/dependency_links.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying hilbert_cuda.egg-info/top_level.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
writing build/bdist.linux-x86_64/egg/EGG-INFO/native_libs.txt
zip_safe flag not set; analyzing archive contents...
__pycache__.hilbert_cuda.cpython-38: module references __file__
creating dist
creating 'dist/hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg' and adding 'build/bdist.linux-x86_64/egg' to it
removing 'build/bdist.linux-x86_64/egg' (and everything under it)
Processing hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg
creating /usr/local/lib/python3.8/site-packages/hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg
Extracting hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg to /usr/local/lib/python3.8/site-packages
Adding hilbert-cuda 0.0.0 to easy-install.pth file

Installed /usr/local/lib/python3.8/site-packages/hilbert_cuda-0.0.0-py3.8-linux-x86_64.egg
Processing dependencies for hilbert-cuda==0.0.0
Finished processing dependencies for hilbert-cuda==0.0.0

test_setup.py

python 复制代码
import torch
import hilbert_cuda  # 导入编译好的扩展模块
import os
import time
import random

def encode_cuda_optimized(locs, num_dims, num_bits):
    device = locs.device
    N = locs.shape[0]
    
    # 格式化输入
    locs_uint8 = locs.contiguous().view(torch.uint8).reshape(N, num_dims, 8)
    
    # 分配输出 
    output = torch.empty(N, dtype=torch.int64, device=device)
    
    # 执行 kernel,并传递 output
    with torch.cuda.device(device):
        hilbert_cuda.hilbert_encode_cuda_optimized(locs_uint8, num_dims, num_bits, output)
        torch.cuda.synchronize()
    
    return output

def right_shift(binary, k=1, axis=-1):
    """Right shift an array of binary values."""
    if binary.shape[axis] <= k:
        return torch.zeros_like(binary)
    slicing = [slice(None)] * len(binary.shape)
    slicing[axis] = slice(None, -k)
    shifted = torch.nn.functional.pad(
        binary[tuple(slicing)], (k, 0), mode="constant", value=0
    )
    return shifted

def gray2binary_old(gray, axis=-1):
    """Convert an array of Gray codes back into binary values."""
    shift = 2 ** (torch.Tensor([gray.shape[axis]]).log2().ceil().int() - 1)
    while shift > 0:
        gray = torch.logical_xor(gray, right_shift(gray, shift))
        shift = torch.div(shift, 2, rounding_mode="floor")
    return gray

def encode(locs, num_dims, num_bits):
    """CPU 实现的 Hilbert 编码"""
    orig_shape = locs.shape
    bitpack_mask = 1 << torch.arange(0, 8, device=locs.device)
    bitpack_mask_rev = bitpack_mask.flip(-1)
    
    if orig_shape[-1] != num_dims:
        raise ValueError(
            f"The shape of locs was surprising in that the last dimension was of size {orig_shape[-1]}, but num_dims={num_dims}. These need to be equal."
        )
    
    if num_dims * num_bits > 63:
        raise ValueError(
            f"num_dims={num_dims} and num_bits={num_bits} for {num_dims * num_bits} bits total, which can't be encoded into a int64. Are you sure you need that many points on your Hilbert curve?"
        )
    
    locs_uint8 = locs.long().view(torch.uint8).reshape((-1, num_dims, 8)).flip(-1)
    gray = (
        locs_uint8.unsqueeze(-1)
        .bitwise_and(bitpack_mask_rev)
        .ne(0)
        .byte()
        .flatten(-2, -1)[..., -num_bits:]
    )
    
    for bit in range(0, num_bits):
        for dim in range(0, num_dims):
            mask = gray[:, dim, bit]
            gray[:, 0, bit + 1 :] = torch.logical_xor(
                gray[:, 0, bit + 1 :], mask[:, None]
            )
            to_flip = torch.logical_and(
                torch.logical_not(mask[:, None]).repeat(1, gray.shape[2] - bit - 1),
                torch.logical_xor(gray[:, 0, bit + 1 :], gray[:, dim, bit + 1 :]),
            )
            gray[:, dim, bit + 1 :] = torch.logical_xor(
                gray[:, dim, bit + 1 :], to_flip
            )
            gray[:, 0, bit + 1 :] = torch.logical_xor(
                gray[:, 0, bit + 1 :], to_flip
            )
    
    gray = gray.swapaxes(1, 2).reshape(-1, num_bits * num_dims)
    hh_bin = gray2binary_old(gray)
    extra_dims = 64 - num_bits * num_dims
    padded = torch.nn.functional.pad(hh_bin, (extra_dims, 0), "constant", 0)
    hh_uint8 = (
        (padded.flip(-1).reshape((-1, 8, 8)) * bitpack_mask)
        .sum(2)
        .squeeze()
        .type(torch.uint8)
    )
    hh_uint64 = hh_uint8.view(torch.int64).squeeze()
    return hh_uint64.reshape(orig_shape[:-1])

def encode_optimized(locs, num_dims, num_bits):
    """
    与 encode 数值等价、性能更优的 Hilbert 曲线编码,支持 GPU 并行。
    """
    shp = locs.shape
    if num_dims * num_bits > 63:
        raise ValueError("Cannot encode more than 63 bits in total.")
    device = locs.device

    locs_uint8 = locs.view(torch.uint8).reshape(-1, num_dims, 8)
    bitpack_mask = 1 << torch.arange(0, 8, device=device)
    bitpack_mask_rev = bitpack_mask.flip(-1)

    gray = (
        locs_uint8.unsqueeze(-1)
        .bitwise_and(bitpack_mask_rev)
        .ne(0)
        .byte()
        .flatten(-2, -1)[..., -num_bits:]
        .reshape(-1, num_dims, num_bits)
    )

    # Skilling 变换
    for bit in range(num_bits):
        for dim in range(num_dims):
            mask = gray[:, dim, bit]
            if bit + 1 < num_bits:
                gray[:, 0, bit+1:] ^= mask.unsqueeze(-1)
                to_flip = (~mask.unsqueeze(-1)) & (gray[:, 0, bit+1:] ^ gray[:, dim, bit+1:])
                gray[:, dim, bit+1:] ^= to_flip
                gray[:, 0, bit+1:] ^= to_flip

    # Gray 转 Binary
    gview = gray.swapaxes(1, 2).reshape(-1, num_bits * num_dims)
    binary = torch.cumsum(gview, dim=-1) % 2
    binary = binary.long()

    # 填充到 64 位并打包
    extra_bits = 64 - num_bits * num_dims
    padded = torch.nn.functional.pad(binary, (extra_bits, 0)).long()
    bitmask = 1 << torch.arange(64, device=device).long()
    hilbert_indices = (padded.flip(-1) * bitmask).sum(dim=-1).long()
    return hilbert_indices.reshape(shp[:-1])

# 测试用例
def main():
    torch.manual_seed(41)  # 可修改

    # 假设我们有 3 个维度
    num_dims = 3  # 3维

    npoints = 10
    total_time_encode = 0
    total_time_encode_new = 0
    num_runs = 10  # 可根据需要调整测试次数

    for i in range(0, num_runs):
        num_bits = random.randint(3, 20)  
        torch.manual_seed(i)
        locs1 = torch.randint(0, 2**num_bits, (npoints, num_dims), dtype=torch.int64)
        locs2 = locs1.to('cuda')
        num_bits = depth = int(torch.max(locs1)).bit_length()
        print(f"Run {i+1}: num_bits = {num_bits}")
        
        # 测试 encode
        start_time = time.time()
        encoded_values_encode = encode(locs1, num_dims, num_bits)
        end_time = time.time()
        total_time_encode += (end_time - start_time)

        # 测试 encode_optimized
        start_time = time.time()
        encoded_values_encode_new = encode_cuda_optimized(locs2, num_dims, num_bits)
        end_time = time.time()
        total_time_encode_new += (end_time - start_time)

        encoded_values_encode_new_cpu = encoded_values_encode_new.cpu()
        # 比较 encode 和 encode_new 的结果
        if torch.equal(encoded_values_encode, encoded_values_encode_new_cpu):
            print("encode 和 encode_new 生成的编码结果完全相同。")
        else:
            print("encode 和 encode_new 生成的编码结果存在差异。")
            # 计算不匹配的数量
            mismatches = (encoded_values_encode != encoded_values_encode_new_cpu).sum().item()
            print(f"不匹配的元素数量: {mismatches}")

            if mismatches > 0:
                mismatched_indices = (encoded_values_encode != encoded_values_encode_new_cpu).nonzero(as_tuple=True)[0]

                num_to_print = min(10, mismatches)
                print(f"前 {num_to_print} 个不匹配的示例:")
                for idx in mismatched_indices[:num_to_print]:
                    print(f"索引 {idx.item()}: encode = {encoded_values_encode[idx].item()}, encode_new = {encoded_values_encode_new_cpu[idx].item()}")

    # 分别打印 encode 以及 encode_optimized 测试耗时
    print("encode 耗时: ", total_time_encode / num_runs)
    print("encode_optimized 耗时: ", total_time_encode_new / num_runs)

if __name__ == "__main__":
    main()
相关推荐
隐擎fox1 小时前
解密 BGP Anycast 边缘路由与跨国链路质量检测:多租户网络出口的低时延与防丢包架构实战
网络·python·网络协议·tcp/ip·网络安全·架构·tcpdump
把我格子衫拿来1 小时前
手眼标定:相机与机械臂如何建立坐标关系?从 AX=XB 到 OpenCV 实践
python·opencv·机器人·手眼标定·坐标变换
暖焰核心1 小时前
多态全解——虚函数、重写、虚表与多态原理
c++
把我格子衫拿来1 小时前
相机标定误差很小,为什么测量仍不准?从 OpenCV 重投影误差到独立验证
python·数码相机·opencv·计算机视觉·相机标定
goehou1 小时前
流式输出实战:SSE 解析、chunk 拼装与断流重试
python·ai·llm·sse·流式
白杨尚青2 小时前
C++入门篇(十八):模板进阶——非类型参数、特化全家桶与分离编译(收官篇)
开发语言·c++·笔记·模板·偏特化·分离编译
SamChan902 小时前
图片翻译后文字挤出边框?用 Pillow 测量译文,不再靠字符数猜宽度
图像处理·python·pillow·本地化·图片翻译
2601_962780912 小时前
2027风险管理岗准备路线:数学与应用数学专业如何补齐金融、SQL与Python
python·sql·金融
老唐7772 小时前
Transformer原理大白话讲解
人工智能·python·深度学习·神经网络·计算机视觉·ai·transformer