【问题标题】:PyCUDA 2D array implementations (or working with strings)PyCUDA 2D 数组实现(或使用字符串)
【发布时间】:2018-06-12 09:26:37
【问题描述】:

我正在尝试在 CUDA 中使用字符串数组(words)。

我尝试通过创建单个字符串来将其展平,但随后要对其进行索引,每次内核运行时我都必须对其进行一些处理。如果有 9000 个字长为 6 个字符,那么在最坏的情况下,我必须检查每个内核调用的 53994 个字符。所以我正在寻找不同的方法来做到这一点。

更新:忘了说,字符串的长度不同,所以我必须找到每个字符串的结尾。

接下来我尝试将每个单词复制到不同的内存位置,然后收集地址,并将其作为数组传递给 GPU,代码如下:

# np = numpy

wordList = ['asd','bsd','csd']

d_words = []

for word in wordList:
    d_words.append(gpuarray.to_gpu(np.array(word, dtype=str)))

d_wordList = gpuarray.to_gpu(np.array([word.ptr for word in d_words], dtype=np.int32))

ker_test(d_wordList, block=(1,1,1), grid=(1,1,1))

在内核中:

__global__ void test(char** d_wordList) {
    printf("First character of the first word is: %c \n", d_wordList[0][0]);
}

内核应该得到一个指向每个单词开头的 int32 指针数组,实际上是一个 char**(或 int**),但它并没有像我预期的那样工作。

这种方法有什么问题?

一般来说,在 PyCUDA(甚至在 CUDA)中处理字符串的“标准”方法是什么?

提前致谢。

【问题讨论】:

  • 目前还不清楚为什么压扁字符串会导致算法困难。我称之为“标准”方法。也不清楚为什么您认为指针数组适合 int32 数组。您在使用 32 位操作系统吗?
  • 忘了提到字符串的长度不同,所以我必须检查每个字符是否是每个“索引”的字符串结尾。这看起来很乏味,写起来也可能计算起来。所以第二种方法会更容易使用,但我不知道它会产生什么性能差异。啊,int32 可能是个问题,出于某种原因认为它就足够了。
  • 除了传递字符串的压缩数组外,还要传递每个字符串的起始偏移量(不是指针)的压缩数组。在打包字符串时应该很容易组装,并且应该很容易传递(作为 int32 数组),并且应该很容易(并且更有效)使用它在 CUDA 内核中进行基于指针的访问本身,而不是使用您在此处讨论的双指针方法。每个字符串的长度只是一个索引/偏移量与下一个索引/偏移量之间的差异。
  • 谢谢!指针大小确实是问题所在,我不知道为什么我认为内存地址是 32 位。传递数组偏移量的想法看起来也很方便,我也应该考虑一下。所以现在我有两个选项可供选择,所有问题都解决了。我不想删除这个问题,但万一有人遇到类似的问题,所以如果你发布一个答案,那么我可以正确地“关闭”它。
  • 正确地说,您应该使用 numpy.uintp 作为指针的 dtype。 PyCUDA 在内部就是这样做的

标签: python arrays string cuda pycuda


【解决方案1】:

经过进一步思考,我得出结论,对于这种可变长度字符串的情况,在考虑数据问题时,使用“偏移数组”可能与二维索引(即双指针索引)没有太大区别在内核中访问。两者都涉及到一定程度的间接性。

这是一个演示这两种方法的工作示例:

$ cat t5.py
#!python
#!/usr/bin/env python
import time
import numpy as np
from pycuda import driver, compiler, gpuarray, tools
import math
from sys import getsizeof

import pycuda.autoinit

kernel_code1 = """
__global__ void test1(char** d_wordList) {
      (d_wordList[blockIdx.x][threadIdx.x])++;
}
    """

kernel_code2 = """
__global__ void test2(char* d_wordList, size_t *offsets) {
    (d_wordList[offsets[blockIdx.x] + threadIdx.x])++;
}
    """




mod = compiler.SourceModule(kernel_code1)
ker_test1 = mod.get_function("test1")



wordList = ['asd','bsd','csd']

d_words = []

for word in wordList:
    d_words.append(gpuarray.to_gpu(np.array(word, dtype=str)))

d_wordList = gpuarray.to_gpu(np.array([word.ptr for word in d_words], dtype=np.uintp))

ker_test1(d_wordList, block=(3,1,1), grid=(3,1,1))

for word in d_words:
  result = word.get()
  print result

mod2 = compiler.SourceModule(kernel_code2)
ker_test2 = mod2.get_function("test2")
wordlist2 = np.array(['asdbsdcsd'], dtype=str)
d_words2 = gpuarray.to_gpu(np.array(['asdbsdcsd'], dtype=str))
offsets = gpuarray.to_gpu(np.array([0,3,6,9], dtype=np.uint64))
ker_test2(d_words2, offsets, block=(3,1,1), grid=(3,1,1))
h_words2 = d_words2.get()
print h_words2


$ python t5.py
bte
cte
dte
['btectedte']
$

注意事项:

  1. 对于双指针情况,与 OP 示例的唯一变化是使用 @talonmies 在 cmets 中建议的指针类型为 numpy.uintp

  2. 我不认为数据的双指针访问一定会比与偏移查找方法相关的间接更快或更慢。另一项性能考虑是在将数据从主机复制到设备的领域,反之亦然。我相信双指针方法有效地涉及双向的多次分配和多次复制操作。对于很多字符串,这在主机/设备数据复制操作中会很明显。

  3. 偏移方法的另一个可能的优点是它很容易确定每个字符串的长度 - 只需减去偏移列表中的两个相邻条目。这可能很有用,以便更容易确定有多少线程可以并行处理字符串,而不是让单个线程按顺序处理字符串(或使用内核代码中的方法来确定字符串长度,或通过每个字符串的长度)。

【讨论】:

  • 再次感谢您的详细解答。经过一些测试,我发现偏移数组方法确实是要走的路。由于内存操作,在处理大数据时,char** 方法变得异常缓慢。 (至少我设法实现它的方式)
猜你喜欢
  • 2013-09-19
  • 2016-09-05
  • 2018-06-10
  • 2020-07-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-23
  • 2017-04-04
相关资源
最近更新 更多