簡體   English   中英

Memory 稀疏矩陣和非稀疏矩陣之間的有效點積 numpy 矩陣

[英]Memory efficient dot product between a sparse matrix and a non-sparse numpy matrix

我已經完成了之前提出的類似問題(例如[1] [2] )。 但是,它們都與我的問題完全無關。

我正在嘗試計算兩個大矩陣之間的點積,我必須滿足一些 memory 約束。

我有一個numpy稀疏矩陣,其形狀為 (10000,600000)。 例如,

from scipy import sparse as sps
x = sps.random(m=10000, n=600000, density=0.1).toarray()

第二個 numpy 矩陣的大小為 (600000, 256),僅由 (-1, 1) 組成。

import numpy as np
y = np.random.choice([-1,1], size=(600000, 256))

我需要盡可能低的xy的點積 memory。 速度不是主要問題。

到目前為止,這是我嘗試過的:

Scipy 稀疏格式:

當然,我將 numpy 稀疏矩陣轉換為 scipy csr_matrix 但是,由於 memory 問題,任務仍然被終止。 沒有錯誤,我只是在終端上被殺了。

from scipy import sparse as sps
sparse_x = sps.csr_matrix(x, copy=False)
z = sparse_x.dot(y)
# killed

降低 dtype 精度 + Scipy 稀疏格式:

from scipy import sparse as sps

x = x.astype("float16", copy=False)
y = y.astype("int8", copy=False)

sparse_x = sps.csr_matrix(x, copy=False)
z = sparse_x.dot(y)
# Increases the memory requirement for some reason and dies 

np.einsum

不確定它是否有助於/適用於稀疏矩陣。 在這個答案中發現了一些有趣的東西。 但是,以下也無濟於事:

z = np.einsum('ij,jk->ik', x, y)
# similar memory requirement as the scipy sparse dot

建議?

如果您有任何改進這些的建議。 請告訴我。 此外,我正在考慮以下方向:

  1. 如果我能以某種方式擺脫點積本身,那就太好了。 我的第二個矩陣(即y是隨機生成的,它只有 [-1, 1]。我希望我可以利用它的特性。

  2. 可能將點積分成幾個小點積,然后聚合。

M@x最深的 python 代碼,其中M是稀疏 csr 矩陣, x是密集數組:

In [28]: M._mul_multivector??
Signature: M._mul_multivector(other)
Docstring: <no docstring>
Source:   
    def _mul_multivector(self, other):
        M, N = self.shape
        n_vecs = other.shape[1]  # number of column vectors

        result = np.zeros((M, n_vecs),
                          dtype=upcast_char(self.dtype.char, other.dtype.char))

        # csr_matvecs or csc_matvecs
        fn = getattr(_sparsetools, self.format + '_matvecs')
        fn(M, N, n_vecs, self.indptr, self.indices, self.data,
           other.ravel(), result.ravel())

        return result

這里的fn是編譯后的代碼。 它獲取csr矩陣的屬性數組,作為一維數組的密集數組,以及預分配的result數組 - 也作為扁平view

我想知道創建result時會發生內存錯誤。 因為我懷疑fn使用該內存作為其工作空間,並且不會嘗試使用更多內存。

我知道 sparse@sparse 矩陣乘法是在兩個編譯步驟中完成的。 第一個確定結果的維度和 nnz,第二個實際進行計算。 兩個步驟之間的 python 代碼創建了result數組,就像這里所做的一樣。 而這個@的內存錯誤發生在result分配中。

如果您創建的數組沒有任何內存問題(我已經為准備好的示例大小),為什么不只使用迭代。 點積等效循環可能是一個解決方案,可以用 numba 加速:

@nb.njit  # ("float64[:, ::1](float64[:, ::1], int32[:, ::1])")
def dot(a, b):

    dot_ = np.zeros((a.shape[0], b.shape[1]))
    for i in range(a.shape[1]):
        for j in range(a.shape[0]):
            for k in range(b.shape[1]):
                dot_[j, k] += a[j, i] * b[i, k]
    return dot_

正如沃倫在評論中提到的那樣,結果很容易適合您准備好的示例。

TL;DR :由於臨時 arrays、類型提升以及使用效率低下,SciPy 消耗的 memory 比嚴格需要的多得多。 它也不是很快。 可以優化設置,以便使用更少的 memory,並且可以使用 Numba 高效地執行計算(針對 memory 的使用和時間)。


我有一個 numpy 稀疏矩陣,其形狀為 (10000,600000)。 例如,
x = sps.random(m=10000, n=600000, density=0.1).toarray()

sps.random(...)是一個 COO 稀疏矩陣,所以它在 memory 中相對緊湊。在其上使用.toarray()會使稀疏矩陣轉換為巨大的密集矩陣 事實上,這個生成的密集矩陣 ( x ) 需要10000*600000*8 = 44.7 GiB (因為浮點數的默認類型是 64 位寬)。

這可能會導致 memory 問題。 在某些具有交換 memory 或大 RAM(例如 64 GiB)的機器上,程序可能會慢得多,如果 memory 關閉飽和(例如由於到 Linux 上的 OOM 殺手)。 請注意,當剩余的 memory 非常有限(即 ZRAM)時,Windows 會壓縮 RAM 中的數據。 此方法適用於具有大量零的 arrays。 但是,當稍后分配額外的 arrays 並從 RAM 中讀回密集數組時,操作系統需要從 RAM 中解壓縮數據並且需要更多空間。 如果rest的RAM內容在解壓的時候不能壓縮的那么好,很可能會出現out of memory。

當然,我將 numpy 稀疏矩陣轉換為 scipy csr_matrix

CSR 矩陣編碼為 3 1D arrays:

  • 包含源矩陣的非零值的data數組;
  • 引用當前行的非零項位置的column索引數組;
  • 引用第一項在前兩項中的偏移量的row索引 arrays。

實際上,最后兩個數組是 32 位 integer arrays,可能是 Linux 上的 64 位整數。由於您的輸入矩陣有 10% 的非零值,這意味着data應該采用10000*600000*8*0.1 = 4.5 GiBcolumn在 Linux 上也應該采用相同的大小(因為它們都包含 64 位值)和 2.2 GiB 在 Windows 上, row Linux 上應該采用10000*8 = 78 KiB ,在 Windows 上甚至更小。因此,CSR 矩陣應該在 Linux 上總共占用大約 9 GiB,在 Windows 上占用大約 6.7 GiB。這仍然相當大。

使用copy=False不是一個好主意,因為 CSR 矩陣將引用巨大的初始數組(AFAIK data將引用x )。 這意味着x需要保留在 memory 中,因此生成的 memory 規格約為44.7 GiB + 2.2~4.5 GiB = 46.9~49.2 GiB 更不用說涉及稀疏矩陣的矩陣乘法通常較慢(除非稀疏因子非常小)。 最好復制數組內容,以便僅使用copy=True保留非零值

x.astype("float16", copy=False)

如果不復制數組,就不可能將數組轉換為另一個具有不同數據類型的數組,尤其是當 memory 中每個項目的大小不同時。即使是這樣,這樣做也沒有意義,因為目標是減少輸入的大小以創建一個新數組而不是保留初始數組。

# Increases the memory requirement for some reason and dies

這有兩個原因。

首先,Scipy 尚不支持稀疏矩陣的 float16 數據類型。 您可以通過檢查sparse_x.dtype看到這一點:當xfloat16時它是float32 這是因為大多數平台本身不支持float16數據類型(x86-64 處理器主要支持從/到這種類型的轉換)。 結果,CSR 矩陣data部分比它可能的大兩倍。

其次,Numpy 內部不支持對 arrays 進行不同數據類型的二進制操作。 Numpy 首先轉換二進制運算的輸入,使它們匹配。 為此,它遵循語義規則。 這稱為類型提升 SciPy 通常以相同的方式工作(特別是因為它經常在內部使用 Numpy)。 這意味着int8數組可能會在您的情況下隱式轉換為float32數組,即 memory 中的 4 倍大數組。我們需要深入研究 SciPy 的實現以了解真正發生的事情。


在引擎蓋下

讓我們了解當我們進行矩陣乘法時 SciPy 內部發生了什么。 正如@hpaulj 所指出的, sparse_x.dot(y)調用_mul_multivector來創建 output 數組並調用csr_matvecs 最后一個是 C++ 包裝 function 調用由宏 SPTOOLS_CSR_DEFINE_TEMPLATE 實例化的模板SPTOOLS_CSR_DEFINE_TEMPLATE 這個宏被提供給另一個宏SPTOOLS_FOR_EACH_INDEX_DATA_TYPE_COMBINATION負責從預定義的支持類型列表中生成所有可能的實例。 csr_matvecs的實現可在此處獲得。

基於此代碼,我們可以看到float16數據類型(至少對於稀疏矩陣而言)。 我們還可以看到,在調用 C++ function csr_matvecs時, selfother必須具有相同的類型(類型提升肯定是在調用 C++ function 之前使用像這樣的包裝代碼進行的)。 我們也可以看到C++的實現並沒有特別優化,也比較簡單。 可以使用 Numba 或 Cython 輕松編寫具有相同邏輯和相同性能的代碼,以支持您特定的緊湊型輸入類型。 我們終於可以看到在 C++ 計算部分沒有分配任何東西(僅在 Python 代碼中,當然在 C++ 包裝器中)。


Memory高效執行

首先,這是一個以緊湊的方式設置數組的代碼:

from scipy import sparse as sps
import numpy as np

sparse_x = sps.random(m=10_000, n=600_000, density=0.1, dtype=np.float32)
sparse_x = sps.csr_matrix(sparse_x) # Efficient conversion

y = np.random.randint(0, 2, size=(600_000, 256), dtype=np.int8)
np.subtract(np.multiply(y, 2, out=y), 1, out=y) # In-place modification

減輕臨時 arrays 開銷的一種簡單的純 Numpy 解決方案是按塊計算矩陣乘法。 這是一個逐個計算矩陣的示例:

chunk_count = 4
m, n = sparse_x.shape
p, q = y.shape
assert n == p
result = np.empty((m, q), dtype=np.float16)
for i in range(chunk_count):
    start, end = m*i//chunk_count, m*(i+1)//chunk_count
    result[start:end,:] = sparse_x[start:end,:] @ y

這在我的機器上慢了 <5%,並且需要更少的 memory,因為一次只創建 output 矩陣的 1 個波段的臨時 arrays。 如果您在使用此代碼時仍然遇到 memory 問題,那么請使用以下方法檢查 output 矩陣是否真的適合 RAM: for l in result: l[:] = np.random.rand(l.size) 事實上,創建一個數組並不意味着 memory 空間在 RAM 中保留(參見這篇文章)。

memory 更高效和更快的解決方案是使用Numba或 Cython 來執行 Scipy 手動執行的操作,而無需創建任何臨時數組。 壞消息是 Numba 尚不支持float16數據類型,因此需要使用float32 這是一個實現:

import numba as nb

@nb.njit(['(float32[::1], int32[::1], int32[::1], int8[:,::1])', '(float32[::1], int64[::1], int64[::1], int8[:,::1])'], fastmath=True, parallel=True)
def sparse_compute(x_data, x_cols, x_rows, y):
    result = np.empty((x_rows.size-1, y.shape[1]), dtype=np.float32)
    for i in nb.prange(x_rows.size-1):
        line = np.zeros(y.shape[1], dtype=np.float32)
        for j in range(x_rows[i], x_rows[i+1]):
            factor = x_data[j]
            y_line = y[x_cols[j],:]
            for k in range(line.size):
                line[k] += y_line[k] * factor
        for k in range(line.size):
            result[i, k] = line[k]
    return result

z = sparse_compute(sparse_x.data, sparse_x.indices, sparse_x.indptr, y)

這比以前的解決方案快得多,而且消耗的 memory 也少得多 事實上,它只消耗 10 MiB 來計算結果(比我機器上的初始解決方案少 50-200 倍),並且它比 SciPy 在只有 2 個核心(i7 -3520M)!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM