共有メモリに numpy.ndarray を載せる

4 min読了の目安(約3600字TECH技術記事

TL; DR

  • Python 3.7 対応
  • 機械学習で利用する16bit 浮動小数点 (np.float16)対応
from multiprocessing.sharedctypes import RawArray
import numpy as np

# 載せたい型とサイズの指定
dtype = np.float16
shape = (3,2)

# ここから
try:
    ctype = np.ctypeslib.as_ctypes_type(np.dtype(dtype))
except NotImplementedError:
    for d in (np.int8, np.int16, np.int32, np.int64):
        _d = np.dtype(d)
	if np.dtype(dtype).itemsize == _d.itemsize:
	    ctype = np.ctypeslib.as_ctypes_type(_d)
	    break
    else:
        raise

len = int(np.array(shape, copy=False,dtype="int").prod())
data = np.ctypeslib.as_array(RawArray(ctype, len))
data.shape = shape
array = data.view(dtype)

0. モチベーション

個人開発している強化学習におけるExperience Replayライブラリで、マルチプロセス対応をするために、 numpy.ndarray のデータを共有メモリに配置したかった。

https://zenn.dev/ymd_h/articles/03edcaa47a3b1c

1. 共有メモリの作り方

Python 3.8 以降はバイト数を直接指定できる multiprocessing.shared_memory が標準ライブラリに追加されているが、 Python 3.7 では利用できない。

https://docs.python.org/ja/3/library/multiprocessing.shared_memory.html

Python 3.7 では multiprocessing.sharedctypes.RawArray を利用することができる。
これは、 ctypes に定義されている「C互換の型 または 1文字の型コード」と「要素数」で指定する必要がある

https://docs.python.org/ja/3/library/multiprocessing.html#multiprocessing.sharedctypes.RawArray

2. np.ctypeslib の利用

numpy.dtype から ctypes に変換するのは、 numpy.ctypeslib の関数群を利用することができる。

https://qiita.com/maiueo/items/b3021a8803859d35a46f
https://numpy.org/doc/stable/reference/routines.ctypeslib.html

これで完成したと思っていた。

from multiprocessing.sharedctypes import RawArray
import numpy as np

# 載せたい型とサイズの指定
dtype = np.single
shape = (3,2)

# ここから
ctype = np.ctypeslib.as_ctypes_type(np.dtype(dtype))
len = int(np.array(shape, copy=False,dtype="int").prod())
data = np.ctypeslib.as_array(RawArray(ctype, len))
data.shape = shape # reshape を使うとコピーしうるので、attribute に直代入

3. C非互換型への対応

完成したと思っていたらユーザーから isseue 報告がやってきた。「np.float16」でエラーになると。

https://gitlab.com/ymd_h/cpprb/-/issues/130

NotImplementedError: Converting dtype('float16') to a ctypes type
16bit浮動小数点は、昨今の64bit環境ではC言語に互換型が無いので、変換できずにエラーとなっていた。

機械学習では精度を犠牲にしてデータサイズを小さくすることも多々あるので、 np.float16 非対応だとまずいと思い対策を検討した。

ctypes への変換を経ずに、同じバイト数の共有メモリを無理やり読み替える方法を採用することにした。 ndarray の reintepret は view()dtype を指定することで可能である。

https://stackoverflow.com/questions/22623135/converting-from-numpy-array-of-one-type-to-another-by-re-interpreting-raw-bytes

ctypes に変換できない時に、変数のサイズが 8bit、16bit、32bit、64bit であるかをチェックして該当する型で共有メモリを作成する。 (128bit は環境依存なので対応させていない。)

try:
    ctype = np.ctypeslib.as_ctypes_type(np.dtype(dtype))
except NotImplementedError:
    for d in (np.int8, np.int16, np.int32, np.int64):
        _d = np.dtype(d)
	if np.dtype(dtype).itemsize == _d.itemsize:
            ctype = np.ctypeslib.as_ctypes_type(_d)
            break
    else:
        # どれにもマッチしなければ、再度 NotImplementedError を上げる
        raise

いっそのこと全部 np.byte の倍数でメモリを確保しても良いが、 np.byte もサイズが環境依存で 1byte とも限らないので、可能な限り実装済みの as_ctypes_type にまかせて拾えない部分だけ個別対応することとした。

完成版 (再掲)

from multiprocessing.sharedctypes import RawArray
import numpy as np

# 載せたい型とサイズの指定
dtype = np.float16
shape = (3,2)

# ここから
try:
    ctype = np.ctypeslib.as_ctypes_type(np.dtype(dtype))
except NotImplementedError:
    for d in (np.int8, np.int16, np.int32, np.int64):
        _d = np.dtype(d)
	if np.dtype(dtype).itemsize == _d.itemsize:
	    ctype = np.ctypeslib.as_ctypes_type(_d)
	    break
    else:
        raise

len = int(np.array(shape, copy=False,dtype="int").prod())
data = np.ctypeslib.as_array(RawArray(ctype, len))
data.shape = shape
array = data.view(dtype)