Correction de tableau ordonné en F
Énoncé du problème
L'encodage/décodage Protobuf des tableaux NumPy échoue s'ils sont ordonnés en F
Tests
from mcbackend.npproto.utils import ndarray_to_numpy, ndarray_from_numpy
arrC = numpy.array([
[1,2,3],
[4,5,6],
], order="C")
arrF = numpy.array([
[1,2,3],
[4,5,6],
], order="F")
assert arrC.strides != arrF.strides
numpy.testing.assert_array_equal(
arrC,
ndarray_to_numpy(ndarray_from_numpy(arrC)),
)
numpy.testing.assert_array_equal(
arrF,
ndarray_to_numpy(ndarray_from_numpy(arrF)),
)
Différences de code
mcbackend/__init__.py
diff --git a/mcbackend/__init__.py b/mcbackend/__init__.py
index 35ef7b9..c6a43eb 100644
--- a/mcbackend/__init__.py
+++ b/mcbackend/__init__.py
@@ -1,23 +1,23 @@
"""
A framework agnostic implementation for storage of MCMC draws.
"""
from .backends.numpy import NumPyBackend
from .core import Backend, Chain, Run
from .meta import ChainMeta, Coordinate, DataVariable, ExtendedValue, RunMeta, Variable
# Backends
try:
from .backends import clickhouse
from .backends.clickhouse import ClickHouseBackend
except ModuleNotFoundError:
pass
# Adapters
try:
from .adapters import pymc
from .adapters.pymc import TraceBackend
except ModuleNotFoundError:
pass
-__version__ = "0.2.3"
+__version__ = "0.2.4"
mcbackend/npproto/utils.py
diff --git a/mcbackend/npproto/utils.py b/mcbackend/npproto/utils.py
index 27bc98c..8ae5396 100644
--- a/mcbackend/npproto/utils.py
+++ b/mcbackend/npproto/utils.py
@@ -1,40 +1,46 @@
"""
Helper functions such as converters between ``ndarray`` and ``Ndarray``.
"""
import numpy
from . import Ndarray
def ndarray_from_numpy(arr: numpy.ndarray) -> Ndarray:
dt = str(arr.dtype)
if "datetime64" in dt:
# datetime64 doesn't support the buffer protocol.
# See https://github.com/numpy/numpy/issues/4983
# This is a hack that automatically encodes it as int64.
arr = arr.astype("int64")
+ # With non-C-ordered arrays (e.g. Fortran-ordered) the underlying buffer
+ # does not match the strides of the original array anymore, because
+ # ``bytes(arr.data)`` always returns the data in C-order.
+ # Therefore, the array is made C-contiguous before extracting data and strides.
+ if not arr.flags.c_contiguous:
+ arr = numpy.ascontiguousarray(arr)
return Ndarray(
shape=list(arr.shape),
dtype=dt,
data=bytes(arr.data),
strides=list(arr.strides),
)
def ndarray_to_numpy(nda: Ndarray) -> numpy.ndarray:
arr: numpy.ndarray
if "datetime64" in nda.dtype:
# Backwards conversion: The data was stored as int64.
arr = numpy.ndarray(
buffer=nda.data,
shape=nda.shape,
dtype="int64",
strides=nda.strides,
).astype(nda.dtype)
else:
arr = numpy.ndarray(
buffer=nda.data,
shape=nda.shape,
dtype=numpy.dtype(nda.dtype),
strides=nda.strides,
)
mcbackend/test_npproto.py
diff --git a/mcbackend/test_npproto.py b/mcbackend/test_npproto.py
index fc88d29..fd88e7c 100644
--- a/mcbackend/test_npproto.py
+++ b/mcbackend/test_npproto.py
@@ -1,45 +1,48 @@
from datetime import datetime
import numpy
import pytest
from mcbackend import npproto
from mcbackend.npproto import utils
class TestUtils:
@pytest.mark.parametrize(
"arr",
[
numpy.arange(5),
numpy.random.uniform(size=(2, 3)),
numpy.array(5),
numpy.array(["hello", "world"]),
numpy.array([datetime(2020, 3, 4, 5, 6, 7, 8), datetime(2020, 3, 4, 5, 6, 7, 9)]),
numpy.array(
[datetime(2020, 3, 4, 5, 6, 7, 8), datetime(2020, 3, 4, 5, 6, 7, 9)],
dtype="datetime64",
),
numpy.array([(1, 2), (3, 2, 1)], dtype=object),
+ numpy.array([[1, 2, 3], [4, 5, 6]], order="C"),
+ numpy.array([[1, 2, 3], [4, 5, 6]], order="F"),
+ numpy.arange(12).reshape(3, 4)[::2, ::2],
],
)
def test_conversion(self, arr: numpy.ndarray):
nda = utils.ndarray_from_numpy(arr)
enc = bytes(nda)
dec = npproto.Ndarray().parse(enc)
assert isinstance(dec.data, bytes)
result = utils.ndarray_to_numpy(dec)
numpy.testing.assert_array_equal(result, arr)
pass
@pytest.mark.parametrize("shape", [(5,), (2, 3), (2, 3, 5), (5, 2, 1, 7)])
@pytest.mark.parametrize("order", "CF")
def test_byteorders(self, shape, order):
arr = numpy.arange(numpy.prod(shape)).reshape(shape, order=order)
nda = utils.ndarray_from_numpy(arr)
assert nda.order == "CF"[arr.flags.f_contiguous]
dec = utils.ndarray_to_numpy(nda)
numpy.testing.assert_array_equal(arr, dec)
pass
Message de validation (Commit)
Corriger l'encodage des tableaux NumPy non contigus
`bytes(arr.data)` renvoie toujours le contenu du tampon dans l'ordre C, mais
l'encodage d'un tableau ordonné en Fortran (ou autre ordre non contigu en C) provoque
un aller-retour corrompu via `ndarray_from_numpy` et `ndarray_to_numpy`.
Le tableau est mélangé lorsque ses pas (strides) d'origine sont stockés.
Utilisez `numpy.ascontiguousarray` pour rendre le tableau contigu en C avant
d'extraire les pas et les données. Maintenant, le tampon et les pas restent cohérents.
Ferme #2124
Remarque : La version `0.2.4` ajoute des tests d'aller-retour pour les tableaux ordonnés en C, ordonnés en F, et non contigus.
Demande d'extraction (Pull request)
Résumé
La conversion aller-retour avec Protobuf des tableaux NumPy corrompt silencieusement les données lorsque le tableau source n'est pas contigu en C (par exemple, ordonné en Fortran). Ce correctif garantit que toutes les dispositions de la mémoire survivent à un cycle d'encodage/décodage.
arrF = numpy.array([[1, 2, 3], [4, 5, 6]], order="F")
numpy.testing.assert_array_equal(
arrF,
ndarray_to_numpy(ndarray_from_numpy(arrF)), # échouait avant ce correctif
)
Cause fondamentale
Le chemin d'encodage dans ndarray_from_numpy stocke à la fois le tampon brut et les pas (strides) du tableau d'entrée. Le problème est une discordance entre les deux :
- Encodage :
bytes(arr.data)copie le tampon de mémoire sous-jacent. Pour les tableaux non contigus en C, cette copie est discrètement réorganisée en ordre C. - Pas stockés tels quels : Les
stridesassociés sont enregistrés dans leur format d'origine (non C), car aucune réorganisation ne leur est appliquée. - Décodage :
numpy.ndarray(buffer=..., strides=...)reconstruit le tableau en interprétant le tampon en ordre C avec les pas d'origine, une discordance qui produit un tableau mélangé.
Correctif
Appelez numpy.ascontiguousarray avant d'extraire le tampon et les pas, afin que les deux reflètent la même disposition de mémoire :
mcbackend/npproto/utils.py: Rendre le tableau contigu en C avant quedataetstridesne soient extraits. Les pas stockés correspondent maintenant toujours à la disposition du tampon stocké. Les tableaux qui sont déjà contigus en C ne sont pas affectés.mcbackend/test_npproto.py: Ajouter des cas de test d'aller-retour explicites pour les tableaux ordonnés en C, ordonnés en F et non contigus (tranchés).
Tests
La matrice de test dans TestUtils.test_conversion couvre désormais :
numpy.array([[1, 2, 3], [4, 5, 6]], order="C")numpy.array([[1, 2, 3], [4, 5, 6]], order="F")numpy.arange(12).reshape(3, 4)[::2, ::2](vue non contiguë)
Tous les cas de dtype existants (int, float, str, datetime, object) continuent de passer sans changement.
Remarques
- Met à jour la version à
0.2.4. ndarray_to_numpyest inchangé ; aucune modification n'est nécessaire du côté du décodage.
Comment tester
- Exécutez
pytest mcbackend/test_npproto.py. Les trois nouveaux cas paramétrés (ordre C, ordre F, tranche non contiguë) devraient passer. - Vérifiez que tous les cas de dtype préexistants (int, float, str, datetime, object) passent toujours sans changement.