Skip to content

Commit a1c3bbf

Browse files
committed
[tmva][sofie] Generate ONNX models and refs instead of binaries in repo
The SOFIE ONNX unit tests relied on 145 binary .onnx files checked into tmva/sofie/test/input_models, plus ~108 frozen reference headers (input_models/references/*.ref.hxx) whose expected outputs were computed once from the exact weights inside those binaries. This made it impossible to tell from the repository what the models contain, and impossible to regenerate them (some were exported with pytorch versions as old as 1.5). Replace both with a single script, tmva/sofie/test/generate_input_models.py: * Each model is built with the onnx helper API in a make_<Name>() function, so graph structure, attributes, shapes and initializers are readable and reviewable. Large weight tensors that used to be opaque random blobs are seeded-random via _random_tensor(), with pytorch-like 1/sqrt(fan_in) scaling. * The inputs for the value-based tests are defined in TEST_INPUTS, and the expected outputs are computed with onnx's ReferenceEvaluator and written to references/<Name>.ref next to the generated models. Where the evaluator is wrong or unimplemented (MaxPool with asymmetric padding, Mean with multidirectional broadcasting, bidirectional or batchwise RNN/LSTM/GRU), numpy fallbacks implement the ONNX operator definitions directly. * The script runs as the new SofieGenerateModels_ONNX ctest, which the other SOFIE ONNX tests require as a fixture. CMake gets the model list at configure time from `generate_input_models.py --list`, which works without the onnx module. TestCustomModelsFromONNX.cxx and TestCladAutodiff.cxx read the inputs and expected outputs at runtime through a small reader in test_helpers.h instead of including frozen headers. SOFIE's results are thus checked against an independent implementation instead of a snapshot of its own past output. While migrating, all computed references were validated to reproduce the frozen .ref.hxx values with the original weights before switching to seeded-random weights. The ONNX model tests now require the onnx python package; they are disabled with a warning if it is missing or has the broken version 1.19.0. The TMVA_SOFIE_ONNX.C tutorial input is likewise generated at configure time instead of being copied from the test sources. 🤖 Done with the help of AI.
1 parent 61e7135 commit a1c3bbf

259 files changed

Lines changed: 6086 additions & 4508 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tmva/sofie/test/CMakeLists.txt

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,56 +10,107 @@
1010
############################################################################
1111

1212

13+
# Look for needed Python modules
14+
ROOT_FIND_PYTHON_MODULE(torch)
15+
16+
# onnx 1.19.0 has a bug that makes this version unusable:
17+
# https://github.com/onnx/onnx/issues/7249
18+
# 1.19.1 instead could be used
19+
# In that case, we have to disable the "TestSofieModels" test,
20+
# which imports onnx indirectly via torch.onnx
21+
ROOT_FIND_PYTHON_MODULE(onnx)
22+
if (ROOT_ONNX_FOUND AND DEFINED ROOT_ONNX_VERSION)
23+
if(ROOT_ONNX_VERSION VERSION_EQUAL "1.19.0")
24+
message(WARNING "Found broken onnx version ${ROOT_ONNX_VERSION} (see https://github.com/onnx/onnx/issues/7249). Some TMVA SOFIE tests will be disabled.")
25+
set(broken_onnx TRUE)
26+
endif()
27+
endif()
28+
29+
# The ONNX input models are not checked into the repository: they are created
30+
# by generate_input_models.py, which runs as the SofieGenerateModels_ONNX test
31+
# that the other ONNX tests depend on. Set ONNX_MODELS_DIR to use pre-existing
32+
# models from a custom directory instead.
1333
if (NOT ONNX_MODELS_DIR)
14-
set(ONNX_MODELS_DIR input_models)
34+
if (ROOT_ONNX_FOUND AND NOT broken_onnx)
35+
set(ONNX_MODELS_DIR ${CMAKE_CURRENT_BINARY_DIR}/input_models)
36+
set(generate_onnx_models TRUE)
37+
# Get the model names from the generator script (--list does not require
38+
# the onnx module, only a Python interpreter).
39+
execute_process(
40+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/generate_input_models.py --list
41+
OUTPUT_VARIABLE _onnx_model_names
42+
OUTPUT_STRIP_TRAILING_WHITESPACE
43+
RESULT_VARIABLE _onnx_list_status)
44+
if (NOT _onnx_list_status EQUAL 0)
45+
message(FATAL_ERROR "Failed to list the SOFIE ONNX test models with generate_input_models.py")
46+
endif()
47+
string(REPLACE "\n" ";" ONNX_MODEL_NAMES "${_onnx_model_names}")
48+
else()
49+
message(WARNING "The onnx Python module is not usable: the ONNX input models for the TMVA SOFIE tests cannot be generated and the corresponding tests are disabled.")
50+
endif()
51+
else()
52+
get_filename_component(ONNX_MODELS_DIR ${ONNX_MODELS_DIR} ABSOLUTE)
53+
file(GLOB ONNX_FILES "${ONNX_MODELS_DIR}/*.onnx")
54+
set(ONNX_MODEL_NAMES "")
55+
foreach(onnx_file ${ONNX_FILES})
56+
get_filename_component(fname ${onnx_file} NAME_WE)
57+
list(APPEND ONNX_MODEL_NAMES ${fname})
58+
endforeach()
1559
endif()
1660

17-
# Finding .onnx files to be parsed and creating the appropriate code to
18-
# parse all file. It is much faster to combine all parsing in a single executable
19-
# which will avoid initialization time (especially when using ROOT)
20-
set(CAPTURE_STR "EmitModel( \"@1\", \"@2\");")
21-
set(ALL_CAPTURES "")
22-
# Finding .onnx files to be parsed and creating the appropriate command
23-
file(GLOB ONNX_FILES "${ONNX_MODELS_DIR}/*.onnx")
24-
foreach(onnx_file ${ONNX_FILES})
25-
get_filename_component(fname ${onnx_file} NAME_WE)
26-
get_filename_component(fdir ${onnx_file} DIRECTORY)
27-
string(REPLACE "@1" ${onnx_file} cap ${CAPTURE_STR})
28-
string(REPLACE "@2" ${fname} cap ${cap})
29-
list(APPEND ALL_CAPTURES ${cap})
30-
endforeach()
31-
string(REPLACE ";" ";\n" EMIT_CAPTURES "${ALL_CAPTURES}")
32-
configure_file(EmitFromONNX.cxx.in EmitFromONNX_all.cxx @ONLY)
33-
34-
ROOTTEST_GENERATE_EXECUTABLE(emitFromONNX EmitFromONNX_all.cxx
35-
LIBRARIES protobuf::libprotobuf ROOTTMVASofie ROOTTMVASofieParser
36-
FIXTURES_SETUP sofie-compile-models-onnx-build)
37-
38-
# silence protobuf warnings seen in version 3.0 and 3.6. Not needed from protobuf version 3.17
39-
target_compile_options(emitFromONNX PRIVATE -Wno-unused-parameter -Wno-array-bounds)
40-
41-
ROOTTEST_ADD_TEST(SofieCompileModels_ONNX
42-
COMMAND ${CMAKE_COMMAND} -E env ROOTIGNOREPREFIX=1 ./emitFromONNX ${onnx_file} ${CMAKE_CURRENT_BINARY_DIR}/${fname}
43-
FIXTURES_REQUIRED sofie-compile-models-onnx-build
44-
FIXTURES_SETUP sofie-compile-models-onnx
45-
)
46-
47-
# Creating a Google Test
48-
if (BLAS_FOUND) # we need BLAS for compiling the models
49-
ROOT_EXECUTABLE(TestCustomModelsFromONNX TestCustomModelsFromONNX.cxx
50-
LIBRARIES Core GTest::gtest GTest::gtest_main
61+
if (ONNX_MODELS_DIR)
62+
# Creating the appropriate code to parse all ONNX model files. It is much
63+
# faster to combine all parsing in a single executable which will avoid
64+
# initialization time (especially when using ROOT)
65+
set(CAPTURE_STR "EmitModel( \"@1\", \"@2\");")
66+
set(ALL_CAPTURES "")
67+
foreach(fname ${ONNX_MODEL_NAMES})
68+
string(REPLACE "@1" ${ONNX_MODELS_DIR}/${fname}.onnx cap ${CAPTURE_STR})
69+
string(REPLACE "@2" ${fname} cap ${cap})
70+
list(APPEND ALL_CAPTURES ${cap})
71+
endforeach()
72+
string(REPLACE ";" ";\n" EMIT_CAPTURES "${ALL_CAPTURES}")
73+
configure_file(EmitFromONNX.cxx.in EmitFromONNX_all.cxx @ONLY)
74+
75+
ROOTTEST_GENERATE_EXECUTABLE(emitFromONNX EmitFromONNX_all.cxx
76+
LIBRARIES protobuf::libprotobuf ROOTTMVASofie ROOTTMVASofieParser
77+
FIXTURES_SETUP sofie-compile-models-onnx-build)
78+
79+
# silence protobuf warnings seen in version 3.0 and 3.6. Not needed from protobuf version 3.17
80+
target_compile_options(emitFromONNX PRIVATE -Wno-unused-parameter -Wno-array-bounds)
81+
82+
set(compile_models_fixtures sofie-compile-models-onnx-build)
83+
if (generate_onnx_models)
84+
ROOTTEST_ADD_TEST(SofieGenerateModels_ONNX
85+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/generate_input_models.py --outdir ${ONNX_MODELS_DIR}
86+
FIXTURES_SETUP sofie-generate-models-onnx
87+
)
88+
list(APPEND compile_models_fixtures sofie-generate-models-onnx)
89+
endif()
90+
91+
ROOTTEST_ADD_TEST(SofieCompileModels_ONNX
92+
COMMAND ${CMAKE_COMMAND} -E env ROOTIGNOREPREFIX=1 ./emitFromONNX
93+
FIXTURES_REQUIRED ${compile_models_fixtures}
94+
FIXTURES_SETUP sofie-compile-models-onnx
5195
)
52-
ROOTTEST_ADD_TEST(TestCustomModelsFromONNX
53-
EXEC ./TestCustomModelsFromONNX
54-
FIXTURES_REQUIRED sofie-compile-models-onnx)
5596

56-
if (clad)
57-
ROOT_EXECUTABLE(TestCladAutodiff TestCladAutodiff.cxx
97+
# Creating a Google Test
98+
if (BLAS_FOUND) # we need BLAS for compiling the models
99+
ROOT_EXECUTABLE(TestCustomModelsFromONNX TestCustomModelsFromONNX.cxx
58100
LIBRARIES Core GTest::gtest GTest::gtest_main
59101
)
60-
ROOTTEST_ADD_TEST(TestCladAutodiff
61-
EXEC ./TestCladAutodiff
102+
ROOTTEST_ADD_TEST(TestCustomModelsFromONNX
103+
EXEC ./TestCustomModelsFromONNX
62104
FIXTURES_REQUIRED sofie-compile-models-onnx)
105+
106+
if (clad)
107+
ROOT_EXECUTABLE(TestCladAutodiff TestCladAutodiff.cxx
108+
LIBRARIES Core GTest::gtest GTest::gtest_main
109+
)
110+
ROOTTEST_ADD_TEST(TestCladAutodiff
111+
EXEC ./TestCladAutodiff
112+
FIXTURES_REQUIRED sofie-compile-models-onnx)
113+
endif()
63114
endif()
64115
endif()
65116

@@ -70,22 +121,6 @@ if (BLAS_FOUND)
70121
endif()
71122
endif()
72123

73-
# Look for needed Python modules
74-
ROOT_FIND_PYTHON_MODULE(torch)
75-
76-
# onnx 1.19.0 has a bug that makes this version unusable:
77-
# https://github.com/onnx/onnx/issues/7249
78-
# 1.19.1 instead could be used
79-
# In that case, we have to disable the "TestSofieModels" test,
80-
# which imports onnx indirectly via torch.onnx
81-
ROOT_FIND_PYTHON_MODULE(onnx)
82-
if (ROOT_ONNX_FOUND AND DEFINED ROOT_ONNX_VERSION)
83-
if(ROOT_ONNX_VERSION VERSION_EQUAL "1.19.0")
84-
message(WARNING "Found broken onnx version ${ROOT_ONNX_VERSION} (see https://github.com/onnx/onnx/issues/7249). Some TMVA SOFIE tests will be disabled.")
85-
set(broken_onnx TRUE)
86-
endif()
87-
endif()
88-
89124
if (ROOT_TORCH_FOUND AND ROOT_ONNX_FOUND AND NOT broken_onnx)
90125
configure_file(Conv1dModelGenerator.py Conv1dModelGenerator.py COPYONLY)
91126
configure_file(Conv2dModelGenerator.py Conv2dModelGenerator.py COPYONLY)

tmva/sofie/test/TestCladAutodiff.cxx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ constexpr auto modelHeaderSuffix = "_FromONNX_unoptimized.hxx";
22
constexpr auto modelDataSuffix = "_FromONNX_unoptimized.dat";
33
#include "test_helpers.h"
44

5-
#include "input_models/references/Linear_16.ref.hxx"
6-
75
#include "gtest/gtest.h"
86

97
// Test differentiating a fully-connected neural network with Clad.
@@ -12,9 +10,9 @@ TEST(ONNXClad, Linear16)
1210
{
1311
constexpr float TOLERANCE = DEFAULT_TOLERANCE;
1412

15-
// Preparing the standard all-ones input
16-
std::vector<float> input(1600);
17-
std::fill_n(input.data(), input.size(), 1.0f);
13+
SofieReference ref = readReference("Linear_16");
14+
// Mutable copy: the numeric differentiation below perturbs the input values
15+
std::vector<float> input = ref.f32("input0");
1816

1917
ASSERT_INCLUDE_AND_RUN(std::vector<float>, "Linear_16", input);
2018

@@ -135,13 +133,5 @@ float Linear_16_wrapper_num_diff(TMVA_SOFIE_Linear_16::Session const &session, f
135133
ADD_FAILURE() << "Further mismatches suppressed (total mismatches: " << mismatchCount << ")";
136134
}
137135

138-
// Checking output size
139-
EXPECT_EQ(output.size(), sizeof(Linear_16_ExpectedOutput::all_ones) / sizeof(float));
140-
141-
float *correct = Linear_16_ExpectedOutput::all_ones;
142-
143-
// Checking every output value, one by one
144-
for (size_t i = 0; i < output.size(); ++i) {
145-
EXPECT_LE(std::abs(output[i] - correct[i]), TOLERANCE);
146-
}
136+
expectNear(output, ref.f32("output0"), TOLERANCE);
147137
}

0 commit comments

Comments
 (0)