Current section
Files
Jump to
Current section
Files
lib/bumblebee/conversion/pytorch_loader.ex
defmodule Bumblebee.Conversion.PyTorchLoader do
@moduledoc false
@doc """
Loads data saved using [`torch.save`](https://pytorch.org/docs/stable/generated/torch.save.html).
PyTorch tensors are automatically deserialized as Nx tensors.
This function supports both the current zip-based file format and
the legacy format.
"""
@spec load!(Path.t()) :: term()
def load!(path) do
# See https://github.com/pytorch/pytorch/blob/v1.11.0/torch/serialization.py#L607
if zip?(path) do
load_zip!(path)
else
load_legacy!(path)
end
end
defp zip?(path) do
# Check for the "local file header signature"
File.open(path, [:read], &IO.binread(&1, 4)) == {:ok, <<80, 75, 3, 4>>}
end
defp load_zip!(path) do
open_zip!(path, fn unzip ->
file_name_map =
unzip
|> Unzip.list_entries()
|> Map.new(fn %Unzip.Entry{file_name: file_name} ->
# Strip the root dir from the file name
name = file_name |> Path.split() |> Enum.drop(1) |> Enum.join("/")
{name, file_name}
end)
binary = read_zip_file(unzip, Map.fetch!(file_name_map, "data.pkl"))
{term, ""} =
Unpickler.load!(binary,
object_resolver: &object_resolver/1,
persistent_id_resolver: fn
{"storage", storage_type, key, _location, _size} ->
file_name = Map.fetch!(file_name_map, "data/#{key}")
{:storage, storage_type, {:zip, path, file_name}}
end
)
term
end)
end
@doc false
def open_zip!(path, fun) do
zip_file = Unzip.LocalFile.open(path)
try do
{:ok, unzip} = Unzip.new(zip_file)
fun.(unzip)
after
Unzip.LocalFile.close(zip_file)
end
end
@doc false
def read_zip_file(unzip, file_name) do
unzip
|> Unzip.file_stream!(file_name)
|> Enum.to_list()
|> IO.iodata_to_binary()
end
defp object_resolver(%{constructor: "collections.OrderedDict", set_items: items}) do
{:ok, Map.new(items)}
end
# See https://github.com/pytorch/pytorch/blob/v1.11.0/torch/_tensor.py#L271-L280
defp object_resolver(%{
constructor: "torch._utils._rebuild_tensor_v2",
args: [storage, offset, shape, strides, _requires_grad, _backward_hooks]
}) do
{:storage, storage_type, storage} = storage
type = storage_type_to_nx(storage_type)
lazy_tensor = %Bumblebee.Conversion.PyTorchLoader.FileTensor{
shape: shape,
type: type,
offset: offset,
strides: strides,
storage: storage
}
{:ok, lazy_tensor}
end
# See https://github.com/pytorch/pytorch/blob/v1.12.1/torch/_tensor.py#L222-L226
defp object_resolver(%{
constructor: "torch._utils._rebuild_device_tensor_from_numpy",
args: [numpy_array, _type, _device, _requires_grad]
}) do
# Tensors on certain devices are serialized as numpy arrays
{:ok, numpy_array}
end
# See https://github.com/numpy/numpy/blob/v1.23.3/numpy/core/src/multiarray/descriptor.c#L2506-L2508
defp object_resolver(%{constructor: "numpy.dtype", args: [type, false = _align, true = _copy]}) do
{:ok, numpy_type_to_nx(type)}
end
# See https://github.com/numpy/numpy/blob/v1.23.3/numpy/core/src/multiarray/methods.c#L2009-L2014
defp object_resolver(%{
constructor: "numpy.core.multiarray._reconstruct",
state: {_version, shape, type, fortran_order?, data}
}) do
tensor =
if fortran_order? do
# In Fortran order the axes are reversed in memory
shape =
shape
|> Tuple.to_list()
|> Enum.reverse()
|> List.to_tuple()
data |> Nx.from_binary(type) |> Nx.reshape(shape) |> Nx.transpose()
else
data |> Nx.from_binary(type) |> Nx.reshape(shape)
end
{:ok, tensor}
end
defp object_resolver(_object), do: :error
defp storage_type_to_nx(%Unpickler.Global{scope: "torch", name: name}) do
# See https://github.com/pytorch/pytorch/blob/v1.11.0/torch/storage.py#L189-L208
mapping = %{
"DoubleStorage" => {:f, 64},
"FloatStorage" => {:f, 32},
"HalfStorage" => {:f, 16},
"LongStorage" => {:s, 64},
"IntStorage" => {:s, 32},
"ShortStorage" => {:s, 16},
"CharStorage" => {:s, 8},
"ByteStorage" => {:u, 8},
"BoolStorage" => {:u, 8},
"BFloat16Storage" => {:bf, 16},
"ComplexDoubleStorage" => {:c, 128},
"ComplexFloatStorage" => {:c, 64}
}
if type = mapping[name] do
type
else
raise "unsupported PyTorch storage type: #{inspect(name)}"
end
end
defp numpy_type_to_nx(name) do
# See https://numpy.org/doc/stable/reference/generated/numpy.dtype.kind.html#numpy-dtype-kind
mapping = %{
"b1" => {:u, 8},
"i1" => {:s, 8},
"i2" => {:s, 16},
"i4" => {:s, 32},
"i8" => {:s, 64},
"u1" => {:u, 8},
"u2" => {:u, 16},
"u4" => {:u, 32},
"u8" => {:u, 64},
"f2" => {:f, 16},
"f4" => {:f, 32},
"f8" => {:f, 64},
"c8" => {:c, 64},
"c16" => {:c, 128}
}
if type = mapping[name] do
type
else
raise "unsupported NumPy data type: #{inspect(name)}"
end
end
@legacy_magic_number 119_547_037_146_038_801_333_356
defp load_legacy!(path) do
data = File.read!(path)
full_size = byte_size(data)
{@legacy_magic_number, data} = Unpickler.load!(data)
{_protocol_version, data} = Unpickler.load!(data)
{_system_info, data} = Unpickler.load!(data)
binaries = storage_binaries(data, full_size)
{term, _} =
Unpickler.load!(data,
object_resolver: &object_resolver/1,
persistent_id_resolver: fn
{"storage", storage_type, root_key, _location, _size, view_metadata} ->
{_, bit_unit} = storage_type_to_nx(storage_type)
byte_unit = div(bit_unit, 8)
{file_offset, size} = Map.fetch!(binaries, root_key)
storage =
case view_metadata do
nil ->
{:file, path, file_offset, size}
{_view_key, offset, view_size} ->
{:file, path, file_offset + offset * byte_unit, view_size * byte_unit}
end
{:storage, storage_type, storage}
{"module", module, _source_file, _source} ->
module
end
)
term
end
defp storage_binaries(data, full_size) do
# We first do a dry run on the pickle and extract storage metadata,
# then we use that metadata to read the storage binaries that follow
{term, data} =
Unpickler.load!(data,
persistent_id_resolver: fn
{"storage", storage_type, root_key, _location, size, _view_metadata} ->
{_, bit_unit} = storage_type_to_nx(storage_type)
byte_unit = div(bit_unit, 8)
{:storage_info, root_key, {size, byte_unit}}
_other ->
nil
end
)
storage_infos = collect_storage_infos(term, %{})
{storage_keys, data} = Unpickler.load!(data)
offset = full_size - byte_size(data)
{pairs, _offset} =
Enum.map_reduce(storage_keys, offset, fn key, offset ->
{size, byte_unit} = Map.fetch!(storage_infos, key)
bytes = size * byte_unit
# Each storage binary is prefixed with the number of elements,
# stored as integer-little-signed-size(64), hence the 8 bytes.
# See https://github.com/pytorch/pytorch/blob/v1.11.0/torch/csrc/generic/serialization.cpp#L93-L134
start_offset = offset + 8
offset = start_offset + bytes
{{key, {start_offset, bytes}}, offset}
end)
Map.new(pairs)
end
defp collect_storage_infos({:storage_info, key, meta}, storage_infos) do
Map.put(storage_infos, key, meta)
end
defp collect_storage_infos(list, storages) when is_list(list) do
Enum.reduce(list, storages, &collect_storage_infos/2)
end
defp collect_storage_infos(%MapSet{} = set, storages) do
Enum.reduce(set, storages, &collect_storage_infos/2)
end
defp collect_storage_infos(%_{} = struct, storages) do
struct |> Map.values() |> collect_storage_infos(storages)
end
defp collect_storage_infos(%{} = map, storages) do
Enum.reduce(map, storages, &collect_storage_infos/2)
end
defp collect_storage_infos(tuple, storages) when is_tuple(tuple) do
tuple |> Tuple.to_list() |> collect_storage_infos(storages)
end
defp collect_storage_infos(_term, storages) do
storages
end
end