mirror of
https://github.com/pytorch/pytorch.git
synced 2026-10-28 18:56:51 +08:00
Summary:
`ObjLoaderFunc` calls `restoreAccurateTypeTags` on every pickled object that
has a `__setstate__`, unconditionally. For modern archives that call finds
nothing to correct, and it is extremely expensive.
Per [type tag serialization] (`unpickler.h`), the two tag-recovery paths are
mutually exclusive:
- **version <= 2**: recover tags by recursively descending the object graph.
- **version >= 3**: the archive carries container type strings, and the
unpickler applies them as each container is constructed
(`restore_type_tag` -> `restoreContainerTypeTags`, `unpickler.cpp`).
`Unpickler::parse_ivalue` already gates its own call on `version_ <= 2`.
`ObjLoaderFunc` never got the same gate, most likely because its signature had
no way to see the archive version.
## Shape of the change
`ObjLoaderFunc` keeps its **exact existing signature**, so it still converts to
`ObjLoader` and every current caller is untouched. A new
`ObjLoaderFuncWithVersion` takes the archive version and applies the gate;
`ObjLoaderFunc` delegates to it with 0 ("unknown"), which is `<= 2` and so
preserves today's behaviour. Only `ScriptModuleDeserializer::readArchive`
opts in, passing `reader_->version()`.
An earlier revision of this diff instead added a defaulted third parameter to
`ObjLoaderFunc`. That is subtly wrong: default arguments are not part of a
function's type, so the two-argument `ObjLoader` conversion stops working. It
broke `caffe2/torch/fb/distributed/wireSerializer` (`WireSerializer.cpp:366`),
which was caught by a ServiceLab build. The current shape avoids the problem by
not touching the existing signature at all.
## Why it matters
Measured on the CG1 Sigrid predictor (tenant m2125804315), Cogwheel workload
`cogwheel_ipnext_cw_inplace_stream_m2125804315_slatest_default_cg1`,
ServiceLab E1727110318567547:
```
[RESTORE_TAGS] big_call works=1280000009 ptr_works=320000004 scanned=320000004 ms=45442
root_type=Tuple[Tuple[int, float, float, List[Tuple[int, int, int]]], Optional[int], Tensor]
[RESTORE_TAGS] big_call kinds: TensorType:1 TupleType:320000002 ListType:1 FloatType:2 IntType:960000002 OptionalType:1
[RESTORE_TAGS] big_call tags list_ptr_eq=0 list_struct_eq=1 list_differs=0 dict_ptr_eq=0 dict_struct_eq=0 dict_differs=0
```
One `List[Tuple[int, int, int]]` holding 320M tuples. The traversal processes
**1.28 billion** work items, at **45 s per model load**, three loads per test.
Its entire useful output is setting the element type on that one list, to the
value it already had.
The decisive number is `list_differs=0` / `dict_differs=0`, holding on every
call, cumulatively across all 16 calls, on both sides of an A/A run: **no
container needed retagging**. The v>=3 path had already done the work.
## Safety
- The condition is identical to the one `unpickler.cpp` has used for years.
- Every existing caller keeps the old behaviour; only the ScriptModule load
path opts in.
- `logTypeTagPolicyOnce` emits one INFO line recording the archive version and
which path was taken, so the gate is observable in a deployed binary rather
than having to be inferred from timing.
- If the gate were ever wrong it fails loudly and immediately: a container
arrives as `List[Any]` and `__setstate__`'s type check rejects it at load
time, rather than silently mistagging.
Related: D115295360 changed the container inside this function and made it
2.73x slower (115 s vs 42 s per load); D116346069 reverts that. This diff
removes the call for modern archives, which subsumes the problem on that path.
Test Plan:
## Tests
Three cases in `caffe2/test/cpp/jit/test_save_load.cpp`, next to the existing
`SerializationTest.TypeTags`. Added in response to review: the version <= 2
branch had no coverage.
- `ObjLoaderRestoresTypeTagsForLegacyArchive` -- feeds the loader the shape an
unpickled version <= 2 archive produces (right values, `List[Any]` tags) and
asserts the tags are corrected in place, on the nested container as well as
the outer one. This is the branch the review asked about.
- `ObjLoaderWithoutVersionRestoresTypeTags` -- the two-argument `ObjLoaderFunc`
has no version to go on and must stay on the legacy path. This pins the
compatibility promise that existing `ObjLoader` callers depend on.
- `ObjLoaderSkipsTypeTagRestorationForModernArchive` -- an already-tagged state
(what a version >= 3 archive produces) loads untouched; a generically tagged
one is passed through unchanged, so `__setstate__`'s own type check rejects
it. That second half is what makes the skip observable, and documents that
the path is only safe because the unpickler tags containers eagerly.
`test_save_load.cpp` is already listed in both `BUCK` and `CMakeLists.txt`, so
no build-file change was needed and OSS CI picks the cases up.
## How they were verified
**The shared gtest binary could not run them.**
`//caffe2/test/cpp/jit:jit` segfaults during static initialization, before any
test body executes. This is pre-existing and unrelated to this diff -- it
reproduces on an unmodified tree, and a filter matching *zero* tests segfaults
identically:
```
$ ./jit --gtest_filter=ThisMatchesNothingAtAll ; echo $?
Segmentation fault
139
$ ./jit --gtest_list_tests ; echo $?
Segmentation fault
139
```
So the assertions were validated by a standalone binary linking
`//caffe2:torch-cpp` and running the same three scenarios:
```
[version 2: legacy archive, tags restored]
PASS outer element type got=List[int] want=List[int]
PASS inner element type got=int want=int
[no version: two-arg overload keeps legacy behaviour]
PASS outer element type got=List[int] want=List[int]
[version 3: modern archive, traversal skipped]
PASS already-tagged state loads without throwing
PASS tagged element type unchanged got=List[int] want=List[int]
PASS generically tagged state is rejected by __setstate__
PASS untagged element type left alone got=Any want=Any
ALL CHECKS PASSED
```
**Mutation check.** To confirm the legacy case actually exercises the gate
rather than passing trivially, the gate was temporarily broken
(`archive_version <= 2` -> `<= 1`, so version 2 takes the skip path) and the
same binary re-run. It fails, in exactly the way the production code would:
```
__setstate__() Expected a value of type 'List[List[int]]' for argument 'state'
but instead found type 'List[Any]'.
Declaration: __setstate__(__torch__.m self, int[][] state) -> NoneType
# 6 torch::jit::ObjLoaderFuncWithVersion(...)
```
The mutation was reverted and the binary re-run clean before submitting.
To be explicit about the gap: the cases are correct and their logic is
verified, but I have not been able to watch them execute under gtest itself
because of the pre-existing crash in that target. OSS CI will be the first
gtest run.
## Build
Clean, exit code captured directly rather than through a pipe. Includes
`wire_serializer`, which an earlier revision of this diff broke:
```
$ buck2 build fbcode//mode/opt fbcode//caffe2:_libtorch \
fbcode//caffe2/torch/fb/distributed/wireSerializer:wire_serializer > /tmp/b.log 2>&1; echo "REAL_EXIT=$?"
REAL_EXIT=0
```
`arc lint -e extra` reports nothing on the files this diff touches. The two
remaining CLANGTIDY findings (`bugprone-unchecked-optional-access`,
`clang-diagnostic-range-loop-bind-reference`) are pre-existing in
`recreateObject` and are not touched here.
## Cogwheel A/B vs current prod
ServiceLab **E2105961143339521**, workload
`cogwheel_ipnext_cw_inplace_stream_m2125804315_slatest_default_cg1`,
side A = current prod, side B = this diff.
```
[PASSED] [Test Suite] (1417.27s)
[PASSED] test_predictor_ab_response_checker (732.69s)
```
Gate fired, and only on side B, which also proves the candidate binary bound:
```
side b: I0818 18:31:47 import.cpp:77] [jit] restoreAccurateTypeTags: archive_version=4 -> skipped (see [type tag serialization])
side a: (no such line - prod does not carry this change)
```
Startup, measured from `parseSubmodToUVMString` to `Setting fb303 status to ALIVE`:
| side | marker | ALIVE | elapsed |
|---|---|---|---|
| A (current prod) | 18:31:22.220 | 18:35:00.502 | 218.3 s |
| B (this diff) | 18:31:22.199 | 18:33:11.366 | **109.2 s** |
**109.1 s faster per cold start.** Response correctness is unchanged --
`test_predictor_ab_response_checker` compares side B's outputs against prod and
passed, which is the check that would catch a container left mistagged.
For reference, the same measurement from E2245630172674916, which A/B'd the two
containers with identical instrumentation:
| config | parseSubmod -> ALIVE |
|---|---|
| `std::unordered_set` (what D116346069 restores) | 147.4 s |
| `c10::FastSet` (master) | 218.6 s |
| current prod | 218.3 s |
| **this diff** | **109.2 s** |
Two things follow. Current prod's startup profile matches the `c10::FastSet`
arm rather than the `std::unordered_set` arm, so prod is already carrying the
D115295360 regression. And skipping the call outright is ~38 s better than
reverting the container, because it removes the whole traversal rather than
making it cheaper.
Differential Revision: D116540489
Pull Request resolved: https://github.com/pytorch/pytorch/pull/194035
Approved by: https://github.com/frgossen
450 lines
15 KiB
C++
450 lines
15 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <test/cpp/jit/test_utils.h>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <caffe2/serialize/inline_container.h>
|
|
#include <torch/csrc/jit/mobile/module.h>
|
|
#include <torch/csrc/jit/runtime/calculate_necessary_args.h>
|
|
#include <torch/csrc/jit/serialization/export.h>
|
|
#include <torch/csrc/jit/serialization/export_bytecode.h>
|
|
#include <torch/csrc/jit/serialization/import.h>
|
|
#include <torch/csrc/jit/serialization/import_source.h>
|
|
#include <torch/script.h>
|
|
#include <torch/torch.h>
|
|
|
|
#include "caffe2/serialize/istream_adapter.h"
|
|
|
|
namespace torch {
|
|
namespace jit {
|
|
|
|
namespace {
|
|
|
|
Module roundtripThroughMobile(const Module& m) {
|
|
ExtraFilesMap files;
|
|
std::vector<IValue> constants;
|
|
jitModuleToPythonCodeAndConstants(m, &files, &constants);
|
|
CompilationOptions options;
|
|
mobile::Module mobilem = jitModuleToMobile(m, options);
|
|
return jitModuleFromSourceAndConstants(
|
|
mobilem._ivalue(), files, constants, 8);
|
|
}
|
|
|
|
template <class Functor>
|
|
inline void expectThrowsEq(Functor&& functor, const char* expectedMessage) {
|
|
try {
|
|
std::forward<Functor>(functor)();
|
|
} catch (const Error& e) {
|
|
EXPECT_STREQ(e.what_without_backtrace(), expectedMessage);
|
|
return;
|
|
}
|
|
ADD_FAILURE() << "Expected to throw exception with message \""
|
|
<< expectedMessage << "\" but didn't throw";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(SerializationTest, ExtraFilesHookPreference) {
|
|
// Tests that an extra file written explicitly has precedence over
|
|
// extra files written by a hook
|
|
// TODO: test for the warning, too
|
|
const auto script = R"JIT(
|
|
def forward(self):
|
|
x = torch.rand(5, 5)
|
|
x = x.mm(x)
|
|
return x
|
|
)JIT";
|
|
|
|
auto module =
|
|
std::make_shared<Module>("Module", std::make_shared<CompilationUnit>());
|
|
module->define(script);
|
|
std::ostringstream oss;
|
|
std::unordered_map<std::string, std::string> extra_files;
|
|
extra_files["metadata.json"] = "abc";
|
|
SetExportModuleExtraFilesHook([](const Module&) -> ExtraFilesMap {
|
|
return {{"metadata.json", "def"}};
|
|
});
|
|
module->save(oss, extra_files);
|
|
SetExportModuleExtraFilesHook(nullptr);
|
|
|
|
std::istringstream iss(std::move(oss).str());
|
|
caffe2::serialize::IStreamAdapter adapter{&iss};
|
|
std::unordered_map<std::string, std::string> loaded_extra_files;
|
|
loaded_extra_files["metadata.json"] = "";
|
|
auto loaded_module = torch::jit::load(iss, torch::kCPU, loaded_extra_files);
|
|
ASSERT_EQ(loaded_extra_files["metadata.json"], "abc");
|
|
}
|
|
|
|
TEST(SerializationTest, ExtraFileHooksNoSecret) {
|
|
// no secrets
|
|
std::stringstream ss;
|
|
{
|
|
Module m("__torch__.m");
|
|
ExtraFilesMap extra;
|
|
extra["metadata.json"] = "abc";
|
|
m.save(ss, extra);
|
|
}
|
|
ss.seekg(0);
|
|
{
|
|
ExtraFilesMap extra;
|
|
extra["metadata.json"] = "";
|
|
extra["secret.json"] = "";
|
|
jit::load(ss, std::nullopt, extra);
|
|
ASSERT_EQ(extra["metadata.json"], "abc");
|
|
ASSERT_EQ(extra["secret.json"], "");
|
|
}
|
|
}
|
|
|
|
TEST(SerializationTest, ExtraFileHooksWithSecret) {
|
|
std::stringstream ss;
|
|
{
|
|
SetExportModuleExtraFilesHook([](const Module&) -> ExtraFilesMap {
|
|
return {{"secret.json", "topsecret"}};
|
|
});
|
|
Module m("__torch__.m");
|
|
ExtraFilesMap extra;
|
|
extra["metadata.json"] = "abc";
|
|
m.save(ss, extra);
|
|
SetExportModuleExtraFilesHook(nullptr);
|
|
}
|
|
ss.seekg(0);
|
|
{
|
|
ExtraFilesMap extra;
|
|
extra["metadata.json"] = "";
|
|
extra["secret.json"] = "";
|
|
jit::load(ss, std::nullopt, extra);
|
|
ASSERT_EQ(extra["metadata.json"], "abc");
|
|
ASSERT_EQ(extra["secret.json"], "topsecret");
|
|
}
|
|
}
|
|
|
|
TEST(SerializationTest, TypeTags) {
|
|
auto list = c10::List<c10::List<int64_t>>();
|
|
list.push_back(c10::List<int64_t>({1, 2, 3}));
|
|
list.push_back(c10::List<int64_t>({4, 5, 6}));
|
|
auto dict = c10::Dict<std::string, at::Tensor>();
|
|
dict.insert("Hello", torch::ones({2, 2}));
|
|
auto dict_list = c10::List<c10::Dict<std::string, at::Tensor>>();
|
|
for (size_t i = 0; i < 5; i++) {
|
|
auto another_dict = c10::Dict<std::string, at::Tensor>();
|
|
another_dict.insert("Hello" + std::to_string(i), torch::ones({2, 2}));
|
|
dict_list.push_back(another_dict);
|
|
}
|
|
auto tuple = std::tuple<int, std::string>(2, "hi");
|
|
struct TestItem {
|
|
IValue value;
|
|
TypePtr expected_type;
|
|
};
|
|
std::vector<TestItem> items = {
|
|
{list, ListType::create(ListType::create(IntType::get()))},
|
|
{2, IntType::get()},
|
|
{dict, DictType::create(StringType::get(), TensorType::get())},
|
|
{dict_list,
|
|
ListType::create(
|
|
DictType::create(StringType::get(), TensorType::get()))},
|
|
{tuple, TupleType::create({IntType::get(), StringType::get()})}};
|
|
// NOLINTNEXTLINE(performance-for-range-copy)
|
|
for (auto item : items) {
|
|
auto bytes = torch::pickle_save(item.value);
|
|
auto loaded = torch::pickle_load(bytes);
|
|
ASSERT_TRUE(loaded.type()->isSubtypeOf(*item.expected_type));
|
|
ASSERT_TRUE(item.expected_type->isSubtypeOf(*loaded.type()));
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
// A class whose __setstate__ takes a precisely typed nested container, so that
|
|
// tag restoration has something observable to correct.
|
|
Module moduleWithNestedListState() {
|
|
Module m("m");
|
|
m.define(R"JIT(
|
|
def __getstate__(self) -> List[List[int]]:
|
|
return [[1, 2, 3]]
|
|
|
|
def __setstate__(self, state: List[List[int]]) -> None:
|
|
pass
|
|
)JIT");
|
|
return m;
|
|
}
|
|
|
|
// What the unpickler hands to the object loader for an archive that carries no
|
|
// serialized container type strings: the right values, but generic tags.
|
|
c10::impl::GenericList genericallyTaggedNestedList() {
|
|
auto inner = c10::impl::GenericList(AnyType::get());
|
|
inner.push_back(1);
|
|
inner.push_back(2);
|
|
auto outer = c10::impl::GenericList(AnyType::get());
|
|
outer.push_back(inner);
|
|
return outer;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// See [type tag serialization] in unpickler.h. Archives at version 2 or lower
|
|
// carry no container type strings, so the object loader has to re-derive the
|
|
// tags from __setstate__'s schema before __setstate__ can run at all.
|
|
TEST(SerializationTest, ObjLoaderRestoresTypeTagsForLegacyArchive) {
|
|
auto m = moduleWithNestedListState();
|
|
at::StrongTypePtr type(m._ivalue()->compilation_unit(), m.type());
|
|
|
|
auto outer = genericallyTaggedNestedList();
|
|
auto inner = outer.get(0).toList();
|
|
|
|
ObjLoaderFuncWithVersion(type, IValue(outer), /*archive_version=*/2);
|
|
|
|
// Corrected in place, on the nested container as well as the outer one.
|
|
EXPECT_EQ(*outer.elementType(), *ListType::create(IntType::get()));
|
|
EXPECT_EQ(*inner.elementType(), *IntType::get());
|
|
}
|
|
|
|
// The two-argument overload has no version to go on, so it stays on the legacy
|
|
// path. Callers that pass it as an ObjLoader depend on this.
|
|
TEST(SerializationTest, ObjLoaderWithoutVersionRestoresTypeTags) {
|
|
auto m = moduleWithNestedListState();
|
|
at::StrongTypePtr type(m._ivalue()->compilation_unit(), m.type());
|
|
|
|
auto outer = genericallyTaggedNestedList();
|
|
|
|
ObjLoaderFunc(type, IValue(outer));
|
|
|
|
EXPECT_EQ(*outer.elementType(), *ListType::create(IntType::get()));
|
|
}
|
|
|
|
// From version 3 on the unpickler has already applied the serialized type
|
|
// strings, so the loader skips the traversal and passes the state through
|
|
// untouched.
|
|
TEST(SerializationTest, ObjLoaderSkipsTypeTagRestorationForModernArchive) {
|
|
auto m = moduleWithNestedListState();
|
|
at::StrongTypePtr type(m._ivalue()->compilation_unit(), m.type());
|
|
|
|
// An already-tagged state, which is what a version >= 3 archive produces,
|
|
// loads without the traversal.
|
|
auto tagged_inner = c10::impl::GenericList(IntType::get());
|
|
tagged_inner.push_back(1);
|
|
auto tagged = c10::impl::GenericList(ListType::create(IntType::get()));
|
|
tagged.push_back(tagged_inner);
|
|
ObjLoaderFuncWithVersion(type, IValue(tagged), /*archive_version=*/3);
|
|
EXPECT_EQ(*tagged.elementType(), *ListType::create(IntType::get()));
|
|
|
|
// A generically tagged state is left exactly as it arrived. Nothing corrects
|
|
// it, so __setstate__'s own type check is what rejects it -- which is why
|
|
// this path is only safe once the unpickler tags containers eagerly.
|
|
auto untagged = genericallyTaggedNestedList();
|
|
EXPECT_ANY_THROW(
|
|
ObjLoaderFuncWithVersion(type, IValue(untagged), /*archive_version=*/3));
|
|
EXPECT_EQ(*untagged.elementType(), *AnyType::get());
|
|
}
|
|
|
|
TEST(SerializationTest, TestJitStream_CUDA) {
|
|
torch::jit::Module model;
|
|
std::vector<torch::jit::IValue> inputs;
|
|
// Deserialize the ScriptModule from a file using torch::jit::load().
|
|
// Load the scripted model. This should have been generated by tests_setup.py
|
|
// Refer: TorchSaveJitStream_CUDA in test/cpp/jit/tests_setup.py
|
|
model = torch::jit::load("saved_stream_model.pt");
|
|
|
|
auto output = model.forward(inputs);
|
|
const auto& list_of_elements = output.toTupleRef().elements();
|
|
auto is_stream_s = list_of_elements[0].toBool();
|
|
|
|
// a,b: These are the two input tensors
|
|
// c: This is output tensor generated by the operation torch.cat(a,b)
|
|
auto a = list_of_elements[1].toTensor();
|
|
auto b = list_of_elements[2].toTensor();
|
|
auto c = list_of_elements[3].toTensor();
|
|
// op: this is used to verify if the cat operation produced the same results
|
|
// as that on the GPU with torch.cat
|
|
auto op = at::cat({a, b}, 0);
|
|
|
|
// Check if the stream is set
|
|
ASSERT_TRUE(is_stream_s);
|
|
// Check if the sizes of the outputs (op and c) is same on the GPU and CPU
|
|
ASSERT_EQ(op.sizes(), c.sizes());
|
|
// Check if both the output tensors are equal
|
|
ASSERT_TRUE(op.equal(c));
|
|
}
|
|
|
|
TEST(TestSourceRoundTrip, UpsampleNearest2d) {
|
|
Module m("m");
|
|
m.define(R"(
|
|
def forward(self, input: Tensor, scale:float):
|
|
return torch.upsample_nearest2d(input, [1, 1], float(scale), float(scale))
|
|
)");
|
|
|
|
std::vector<IValue> inputs;
|
|
inputs.emplace_back(torch::rand({1, 3, 128, 128}));
|
|
inputs.emplace_back(at::Scalar(2.0));
|
|
auto ref = m.forward(inputs);
|
|
|
|
Module m2 = roundtripThroughMobile(m);
|
|
auto res = m2.forward(inputs);
|
|
|
|
auto resd = res.toTensor();
|
|
auto refd = ref.toTensor();
|
|
ASSERT_TRUE(resd.equal(refd));
|
|
}
|
|
|
|
TEST(TestSourceRoundTrip, CheckAttrAccess) {
|
|
Module m("m");
|
|
m.register_attribute("mobile_optimized", BoolType::get(), true);
|
|
Module m2 = roundtripThroughMobile(m);
|
|
bool mobile_optimized = m2.attr("mobile_optimized", false).toBool();
|
|
AT_ASSERT(mobile_optimized);
|
|
}
|
|
|
|
TEST(TestSourceRoundTrip,
|
|
MethodInvocation) { // NOLINT (use =delete in gtest)
|
|
const std::vector<std::string> test_programs{
|
|
// test invoking a method with default parameter
|
|
R"(
|
|
def test_func(self, x, b : int = 4):
|
|
return self.foo + x + b
|
|
)",
|
|
// inner method call with default parameter (gets inlined)
|
|
R"(
|
|
def add_with_default_arg(self, x, b : int = 4):
|
|
return self.foo + x + b
|
|
def test_func(self, x):
|
|
return self.add_with_default_arg(x) # invoke method w/ default arg
|
|
)",
|
|
// simple method call
|
|
R"(
|
|
def test_func(self, x):
|
|
b = 4
|
|
return self.foo + x + b
|
|
)",
|
|
};
|
|
for (const auto& test_program : test_programs) {
|
|
Module m("m");
|
|
m.register_parameter("foo", torch::ones({}), false);
|
|
m.define(test_program);
|
|
|
|
const int fortyTwo = 42; // (keep linter happy)
|
|
auto minput = fortyTwo * torch::ones({});
|
|
auto ref = m.run_method("test_func", minput);
|
|
|
|
Module m2 = roundtripThroughMobile(m);
|
|
const auto& test_func = m2.get_method("test_func");
|
|
IValue res;
|
|
for (int i = 0; i < 3; ++i) {
|
|
res = test_func({minput});
|
|
}
|
|
|
|
auto resd = res.toTensor().item<float>();
|
|
auto refd = ref.toTensor().item<float>();
|
|
AT_ASSERT(resd == refd);
|
|
}
|
|
}
|
|
|
|
TEST(SerializationTest, ParentDirNotExist) {
|
|
expectThrowsEq(
|
|
[]() {
|
|
auto t = torch::nn::Linear(5, 5);
|
|
torch::save(t, "./doesnotexist/file.pt");
|
|
},
|
|
"Parent directory ./doesnotexist does not exist.");
|
|
}
|
|
|
|
#ifdef WIN32
|
|
TEST(SerializationTest, WindowsDrivePathTest) {
|
|
// "ZZZ" is typically not a valid drive letter.
|
|
// We expect to see "ZZZ:\\" or "ZZZ:/" in the error message.
|
|
// Note: slash should be included for the drive letter parent in Windows.
|
|
expectThrowsEq(
|
|
[]() {
|
|
auto t = torch::nn::Linear(5, 5);
|
|
torch::save(t, "ZZZ:\\file.pt");
|
|
},
|
|
"Parent directory ZZZ:\\ does not exist.");
|
|
expectThrowsEq(
|
|
[]() {
|
|
auto t = torch::nn::Linear(5, 5);
|
|
torch::save(t, "ZZZ:/file.pt");
|
|
},
|
|
"Parent directory ZZZ:/ does not exist.");
|
|
}
|
|
|
|
TEST(SerializationTest, WindowsTempPathTest) {
|
|
// Test for verifying file saving and loading in the temporary folder
|
|
std::string temp_dir = std::getenv("TEMP");
|
|
std::string file_path = temp_dir + "/file.pt";
|
|
auto t1 = torch::tensor(1.0);
|
|
torch::save(t1, file_path);
|
|
torch::Tensor t2;
|
|
torch::load(t2, file_path);
|
|
ASSERT_TRUE(t1.allclose(t2, 0.0, 0.0));
|
|
}
|
|
#endif
|
|
|
|
TEST(SerializationTest, CalculateNecessaryArgsTest) {
|
|
auto schema = torch::schema(
|
|
"sync_stream(int stream_id = -1) -> ()",
|
|
c10::AliasAnalysisKind::CONSERVATIVE);
|
|
|
|
auto graph = std::make_shared<Graph>();
|
|
auto one_val = graph->insertConstant(-1);
|
|
auto necessary = CalculateNecessaryArgs(schema.arguments(), {one_val}, true);
|
|
EXPECT_EQ(0, necessary.first);
|
|
EXPECT_EQ(0, necessary.second);
|
|
}
|
|
|
|
TEST(TestSaveLoad, LoadWithoutDebugInfo) { // NOLINT (use =delete in gtest)
|
|
Module m("m");
|
|
m.register_parameter("foo", torch::ones({}), false);
|
|
m.define(
|
|
R"(
|
|
def test_func(self, x):
|
|
b = 4
|
|
return self.foo + x + b
|
|
)");
|
|
m.define(
|
|
R"(
|
|
def exception(self):
|
|
assert False, "message"
|
|
)");
|
|
std::stringstream ss;
|
|
m.save(ss);
|
|
ss.seekg(0);
|
|
caffe2::serialize::PyTorchStreamReader reader(&ss);
|
|
reader.setShouldLoadDebugSymbol(true);
|
|
EXPECT_TRUE(reader.hasRecord("code/__torch__.py.debug_pkl"));
|
|
reader.setShouldLoadDebugSymbol(false);
|
|
EXPECT_FALSE(reader.hasRecord("code/__torch__.py.debug_pkl"));
|
|
ss.seekg(0);
|
|
Module m2 = torch::jit::load(ss);
|
|
std::string error_msg = R"(
|
|
def exception(self):
|
|
assert False, "message"
|
|
~~~~~~~~~~~~~~~~~~~~~~~ <--- HERE)";
|
|
ASSERT_THROWS_WITH_MESSAGE(m2.run_method("exception"), error_msg);
|
|
|
|
ss.seekg(0);
|
|
// NO DEBUG trace so error message points to torchscript generated
|
|
// source instead of original python source.
|
|
std::string error2 = R"(
|
|
def exception(self: __torch__.m) -> NoneType:
|
|
_0 = uninitialized(NoneType)
|
|
ops.prim.RaiseException("AssertionError: message")
|
|
~~~~~~~~~~~~~~~~~~~~~~~ <--- HERE
|
|
return _0
|
|
)";
|
|
Module m3 = torch::jit::load(ss, std::nullopt, false);
|
|
ASSERT_THROWS_WITH_MESSAGE(m3.run_method("exception"), error2);
|
|
}
|
|
|
|
TEST(SerializationTest, TestPickleAppend) {
|
|
auto data = std::vector<char>({'\x80', char(2), ']', 'K', char(2), 'a', '.'});
|
|
|
|
torch::IValue actual = torch::jit::unpickle(data.data(), data.size());
|
|
|
|
torch::IValue expected = c10::impl::GenericList(at::AnyType::get());
|
|
expected.toList().push_back(2);
|
|
ASSERT_EQ(expected, actual);
|
|
}
|
|
|
|
} // namespace jit
|
|
} // namespace torch
|