Hi, and thanks to all keras-team :)
The get_config
of TorchModuleWrapper
uses BytesIO
and torch.save
to create a byte array:
https://github.com/keras-team/keras/blob/1137074a9a1c237473f2fe57ab277c697892c6f1/keras/utils/torch_utils.py#L140
In the serialization lib, bytes
object are then decoded to utf-8:
https://github.com/keras-team/keras/blob/1137074a9a1c237473f2fe57ab277c697892c6f1/keras/saving/serialization_lib.py#L154
An arbitrary byte array, as created by torch.save
may fail decoding as it is not a valid utf-8 string, and will cause an exception on saving.
I think that the real issue here is the decoding of arbitrary bytes to a string, but I assume this is something that will be a hard to fix as it will create format compatibility issues, as it seems to date back a few years ago to keras 2.
Maybe the torch model buffer can be dumped to a json string and read from it by default, or dumped to str
and then use ast.literal_eval
or some other safe binary<->string conversion.
Comment From: SuryanarayanaY
Hi @gilfree , Thanks for reporting. It would be helpful if you could submit a minimal reproducible snippet to check the behaviour. Thanks!
Comment From: gilfree
To reproduce, add the following to the file: keras/utils/torch_utils_test.py:
def test_save_load(self):
import keras
class M(keras.Model):
def __init__(self,channels=10, **kwargs):
super().__init__()
self.sequence = torch.nn.Sequential(
torch.nn.Conv2d(1, channels, kernel_size=(3, 3)),
)
def call(self, x):
return self.sequence(x)
def get_config(self):
return self.sequence.get_config()
m = M()
x=torch.ones((10,1,28, 28))
m(x)
m.save('model.keras')
Run with:
CUDA_VISIBLE_DEVICES=-1 KERAS_BACKEND=torch pytest -vvv -vs keras/utils/torch_utils_test.py -k save_load
This is an artificial example as one will probably not write this get config, just to demonstrate the issue in the shortest way I have found.
Comment From: SuryanarayanaY
Hi @gilfree ,
I have added the code snippet you provided to the file: keras/utils/torch_utils_test.py
and replicated the reported error. Attached logs below.
(keras-jax) suryanarayanay-macbookpro:keras suryanarayanay$ CUDA_VISIBLE_DEVICES=-1 KERAS_BACKEND=torch pytest -vvv -vs keras/utils/torch_utils_test.py -k save_load
===================================================================== test session starts ======================================================================
platform darwin -- Python 3.10.13, pytest-7.4.2, pluggy-1.3.0 -- /Users/suryanarayanay/miniconda/envs/keras-jax/bin/python
cachedir: .pytest_cache
rootdir: /Users/suryanarayanay/keraswork
configfile: pyproject.toml
plugins: cov-4.1.0, anyio-4.1.0
collected 0 items
==================================================================== no tests ran in 0.00s =====================================================================
ERROR: file or directory not found: keras/utils/torch_utils_test.py
(keras-jax) suryanarayanay-macbookpro:keras suryanarayanay$
(keras-jax) suryanarayanay-macbookpro:keras suryanarayanay$ CUDA_VISIBLE_DEVICES=-1 KERAS_BACKEND=torch pytest -vvv -vs /Users/suryanarayanay/keraswork/keras/utils/torch_utils_test.py -k save_load
===================================================================== test session starts ======================================================================
platform darwin -- Python 3.10.13, pytest-7.4.2, pluggy-1.3.0 -- /Users/suryanarayanay/miniconda/envs/keras-jax/bin/python
cachedir: .pytest_cache
rootdir: /Users/suryanarayanay/keraswork
configfile: pyproject.toml
plugins: cov-4.1.0, anyio-4.1.0
collected 15 items / 14 deselected / 1 selected
utils/torch_utils_test.py::TorchUtilsTest::test_save_load FAILED
=========================================================================== FAILURES ===========================================================================
________________________________________________________________ TorchUtilsTest.test_save_load _________________________________________________________________
self = <keras.utils.torch_utils_test.TorchUtilsTest testMethod=test_save_load>
def test_save_load(self):
import keras
class M(keras.Model):
def __init__(self,channels=10, **kwargs):
super().__init__()
self.sequence = torch.nn.Sequential(
torch.nn.Conv2d(1, channels, kernel_size=(3, 3)),
)
def call(self, x):
return self.sequence(x)
def get_config(self):
return self.sequence.get_config()
m = M()
x=torch.ones((10,1,28, 28))
m(x)
> m.save('model.keras')
utils/torch_utils_test.py:211:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
utils/traceback_utils.py:113: in error_handler
return fn(*args, **kwargs)
models/model.py:302: in save
return saving_api.save_model(self, filepath, overwrite, **kwargs)
saving/saving_api.py:100: in save_model
saving_lib.save_model(model, filepath)
saving/saving_lib.py:92: in save_model
_save_model_to_fileobj(model, f, weights_format)
saving/saving_lib.py:97: in _save_model_to_fileobj
serialized_model_dict = serialize_keras_object(model)
saving/serialization_lib.py:239: in serialize_keras_object
inner_config = _get_class_or_fn_config(obj)
saving/serialization_lib.py:373: in _get_class_or_fn_config
return serialize_dict(config)
saving/serialization_lib.py:385: in serialize_dict
return {key: serialize_keras_object(value) for key, value in obj.items()}
saving/serialization_lib.py:385: in <dictcomp>
return {key: serialize_keras_object(value) for key, value in obj.items()}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
obj = b'PK\x03\x04\x00\x00\x08\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x12\x00ar...f6\n\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00PK\x05\x06\x00\x00\x00\x00\x06\x00\x06\x00~\x01\x00\x00x\t\x00\x00\x00\x00'
@keras_export(
[
"keras.saving.serialize_keras_object",
"keras.utils.serialize_keras_object",
]
)
def serialize_keras_object(obj):
"""Retrieve the config dict by serializing the Keras object.
`serialize_keras_object()` serializes a Keras object to a python dictionary
that represents the object, and is a reciprocal function of
`deserialize_keras_object()`. See `deserialize_keras_object()` for more
information about the config format.
Args:
obj: the Keras object to serialize.
Returns:
A python dict that represents the object. The python dict can be
deserialized via `deserialize_keras_object()`.
"""
if obj is None:
return obj
if isinstance(obj, PLAIN_TYPES):
return obj
if isinstance(obj, (list, tuple)):
config_arr = [serialize_keras_object(x) for x in obj]
return tuple(config_arr) if isinstance(obj, tuple) else config_arr
if isinstance(obj, dict):
return serialize_dict(obj)
# Special cases:
if isinstance(obj, bytes):
return {
"class_name": "__bytes__",
> "config": {"value": obj.decode("utf-8")},
}
E UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 64: invalid start byte
saving/serialization_lib.py:154: UnicodeDecodeError
=================================================================== short test summary info ====================================================================
FAILED utils/torch_utils_test.py::TorchUtilsTest::test_save_load - UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 64: invalid start byte
=============================================================== 1 failed, 14 deselected in 3.41s ===============================================================
(keras-jax) suryanarayanay-macbookpro:keras suryanarayanay$
Comment From: SuryanarayanaY
Hi @gilfree ,
This might be due to the reason that Torch objects lacks get_config methods for serialization. This seems addressed in this comment.
Please check it once and come back.
Comment From: github-actions[bot]
This issue is stale because it has been open for 14 days with no activity. It will be closed if no further activity occurs. Thank you.
Comment From: gilfree
Iti s correct that torch objects lack get_config. The comment above is the cause of the bug, not the solution to it.
As stated there, Torch modules are automatically wrapped by TorchModuleWrapper. The issue is that the wrapping is buggy. The wrapping "fallbacks" to serialization, which actually might be a good idea, but the serialization code contains a bug.
This makes it impossible to use the auto wrapping of torch modules within keras model to work with save/load - since the user cannot implement a correct get_config in the autowrapping case.
That makes the torch autowrapping pretty useless as I see it - you can't resume from a checkpoint of a model for example.
Comment From: MicheleCattaneo
This bug is currently still present. The following is a minimal snippet that can reproduce it:
import os
os.environ["KERAS_BACKEND"] = "torch"
import torch
import keras
torch_module = torch.nn.Linear(4,4)
keras_layer = keras.layers.TorchModuleWrapper(torch_module)
inputs = keras.Input(shape=(4,))
outputs = keras_layer(inputs)
model = keras.Model(inputs=inputs, outputs=outputs)
model.save('./serialized.keras')
The error is:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 64: invalid start byte
generated in keras.src.saving.serialization_lib.serialize_keras_object
It is worth noting that manually using get_config
and from_config
to serialize and deserialize (in memory) produce the correct result:
torch_linear = torch.nn.Linear(4,4) # xA^T+b with initalized weights
wrapped_torch = TorchModuleWrapper(torch_linear) # Wrap it
# get its config, and rebuild it
torch_linear_from_config = keras.layers.TorchModuleWrapper.from_config(wrapped_torch.get_config()).module
# assert all parameters are the same
assert (torch_linear.weight == torch_linear_from_config.weight).all()
assert (torch_linear.bias == torch_linear_from_config.bias).all()
What get_config()
does is map module
(a torch object) to its serialized string (coming from torch.save(self.module, buffer)
). I believe it is wrong to use the utf-8 in serialize_keras_object(obj), since that encoding is specifically meant for text and not arbitrary bytes.
Does anybody have an idea about it? Thank you for any help on this!
I got this error with both: - python 3.10, keras 3.7.0, torch 2.5.1+cu124 - python 3.11, keras 3.8.0, torch 2.5.1+cu124
Comment From: dnerini
Hi @SuryanarayanaY, just checking in, are you still working on this issue? Tagging @mehtamansi29 and @sachinprasadhs for input on the next steps, does this needs reassignment? Thanks!
Comment From: sonali-kumari1
Hi @gilfree, @MicheleCattaneo -
I have tried to reproduce the UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 64: invalid start byte
reported in this issue, using the following code:
import os
os.environ["KERAS_BACKEND"] = "torch"
import torch
import keras
torch_module = torch.nn.Linear(4,4)
keras_layer = keras.layers.TorchModuleWrapper(torch_module)
inputs = keras.Input(shape=(4,))
outputs = keras_layer(inputs)
model = keras.Model(inputs=inputs, outputs=outputs)
model.save('./serialized.keras')
However, instead of UnicodeDecodeError
, I encountered RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
instead of UnicodeDecodeError
. I tested with both the latest version of Keras(3.10.0) and Pytorch(2.7.1+cu126), as well as with keras(3.8.0) and torch(2.5.1+cu124). Attaching gist for your reference. We will look into this and update you. Thanks!
Comment From: MicheleCattaneo
Hi, Thanks for looking into it! There was as another linked issue with a PR that tried to solve this. In the PR there was a discussion that maybe can be useful to you.