Skip to content

Commit 6bc8a31

Browse files
authored
Say so when the compiled export fails (#1697)
`run_train` saves the model twice: the pickled checkpoint, then a TorchScript copy beside it. The second is wrapped in `try/except Exception`, which is right -- a model TorchScript cannot take is not a reason to lose a finished training -- but the handler was a bare `pass`, three lines after an INFO line announcing the file by name. The failure mode was the log promising `<name>_compiled.model`, no file appearing, and nothing anywhere saying why. Both copies of that handler (the stage-two branch and the plain one) now log a warning naming the file and the exception. Nothing else changes: the run still succeeds, because the pickled model is on disk and that is what the handler was protecting. The test forces the failure rather than simulating it, by putting a directory where the file goes -- `torch.save` uses a different name, so it succeeds and the branch under test is the real one. Reverting the fix fails two of the five.
1 parent b66c396 commit 6bc8a31

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

mace/cli/run_train.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,10 @@ def run(args) -> None:
12041204
_extra_files=extra_files,
12051205
)
12061206
except Exception as e: # pylint: disable=W0718
1207-
pass
1207+
logging.warning(
1208+
f"Compiling the model failed, so {path_complied.name} "
1209+
f"was not written: {e}"
1210+
)
12081211
else:
12091212
torch.save(model_to_save, Path(args.model_dir) / (args.name + ".model"))
12101213
try:
@@ -1219,7 +1222,10 @@ def run(args) -> None:
12191222
_extra_files=extra_files,
12201223
)
12211224
except Exception as e: # pylint: disable=W0718
1222-
pass
1225+
logging.warning(
1226+
f"Compiling the model failed, so {path_complied.name} "
1227+
f"was not written: {e}"
1228+
)
12231229

12241230
logging.info("Computing metrics for training, validation, and test sets")
12251231
for param in model.parameters():
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""What a run says when the compiled export fails.
2+
3+
`run_train` saves the model twice: the pickled checkpoint, then a TorchScript
4+
copy next to it under `<name>_compiled.model`. The second one is wrapped in
5+
`try/except Exception`, which is right -- a model TorchScript cannot take is not
6+
a reason to lose a finished training -- but the handler was a bare `pass`, three
7+
lines after an INFO line announcing the file. So the failure mode was: the log
8+
says the file is being written, no file appears, and nothing anywhere says why.
9+
10+
The failure is forced here rather than simulated, by putting a directory where
11+
the file goes: the pickled save succeeds, `torch.jit.save` cannot open its
12+
target, and the branch under test is the real one.
13+
"""
14+
15+
import ase.io
16+
import pytest
17+
18+
from tests.helpers import base_mace_params, make_fitting_configs, run_mace_train
19+
20+
21+
def train(tmp_path, blocked: bool):
22+
ase.io.write(tmp_path / "fit.xyz", make_fitting_configs())
23+
if blocked:
24+
# Where `torch.jit.save` wants to write its file. `torch.save` of the
25+
# pickled model uses a different name, so it still succeeds and the run
26+
# reaches the compiled step with everything else intact.
27+
(tmp_path / "ce_compiled.model").mkdir()
28+
params = base_mace_params()
29+
params.update(
30+
{
31+
"name": "ce",
32+
"hidden_irreps": "16x0e",
33+
"checkpoints_dir": str(tmp_path),
34+
"model_dir": str(tmp_path),
35+
"results_dir": str(tmp_path),
36+
"log_dir": str(tmp_path),
37+
"train_file": str(tmp_path / "fit.xyz"),
38+
"max_num_epochs": 1,
39+
"seed": 4,
40+
}
41+
)
42+
# The stage-two branch writes `_stagetwo_compiled.model` instead, and the
43+
# two handlers are separate copies of the same code; this exercises the
44+
# plain one.
45+
params.pop("swa", None)
46+
params.pop("start_swa", None)
47+
return run_mace_train(params, check=False, capture_output=True, text=True)
48+
49+
50+
@pytest.fixture(name="blocked_run", scope="module")
51+
def fixture_blocked_run(tmp_path_factory):
52+
work = tmp_path_factory.mktemp("compiled_blocked")
53+
return train(work, blocked=True), work
54+
55+
56+
def test_a_compiled_export_that_cannot_be_written_does_not_fail_the_run(blocked_run):
57+
done, _ = blocked_run
58+
"""The reason the handler exists: the training is finished and the pickled
59+
model is on disk, so a TorchScript copy that cannot be produced is not worth
60+
losing it over."""
61+
assert done.returncode == 0, done.stderr[-3000:]
62+
63+
64+
def test_the_failure_is_reported(blocked_run):
65+
done, _ = blocked_run
66+
"""The defect. A bare `pass` left the announcement of the file as the last
67+
word on the subject."""
68+
assert "ce_compiled.model" in done.stdout
69+
assert "was not written" in done.stdout, done.stdout[-3000:]
70+
71+
72+
def test_the_report_names_the_reason(blocked_run):
73+
done, _ = blocked_run
74+
"""A warning that says only "it failed" sends the reader back to guessing
75+
between a model TorchScript rejects and a path it cannot open."""
76+
warnings = [
77+
line for line in done.stdout.splitlines() if "was not written" in line
78+
]
79+
80+
assert warnings, done.stdout[-2000:]
81+
assert any("WARNING" in line for line in warnings), warnings
82+
assert any(
83+
"Is a directory" in line or "directory" in line.lower() for line in warnings
84+
), warnings
85+
86+
87+
def test_the_pickled_model_is_still_there(blocked_run):
88+
"""What the run does deliver, and the reason the warning is a warning."""
89+
_, work = blocked_run
90+
91+
assert (work / "ce.model").is_file()
92+
assert (work / "ce_compiled.model").is_dir()
93+
94+
95+
def test_an_unobstructed_run_writes_both_and_warns_about_neither(tmp_path):
96+
"""The control: the same run without the directory in the way."""
97+
done = train(tmp_path, blocked=False)
98+
99+
assert done.returncode == 0
100+
assert (tmp_path / "ce.model").is_file()
101+
assert (tmp_path / "ce_compiled.model").is_file()
102+
assert "was not written" not in done.stdout

0 commit comments

Comments
 (0)