From 45b5b1fd79a179d1b599e6c15b96fc6e6e28a782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 1 Sep 2026 16:26:16 +0200 Subject: [PATCH] [executor_runner] Add --input_shapes to run dynamic models below their bound prepare_input_tensors() sizes every input from the bound recorded in the method metadata, so a model exported with dynamic shapes can only ever be executed at its upper bound from this runner. There is no flag to ask for a smaller shape, and --inputs files are required to match nbytes() of the bound, so there is no way to sneak one in either. That matters because the upper bound is exactly the shape at which a whole class of backend bug is invisible. An op that fails to resize its output, or a kernel whose launch geometry disagrees with what it was compiled for, is correct at the bound and wrong everywhere below it. Without this flag those defects cannot be reproduced from the runner at all. Add --input_shapes, taking a comma-separated shape per input with dims joined by 'x' ("1x98,1x50x256,1x1x98"). Inputs are resized after prepare_input_tensors() and before execute(). Input files stay upper-bound sized; the surplus is simply not read, so existing input files keep working unchanged. I used this to find and confirm four ET-VK dynamic-shape bugs; each was correct at the bound and wrong below it. --- .../executor_runner/executor_runner.cpp | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/examples/portable/executor_runner/executor_runner.cpp b/examples/portable/executor_runner/executor_runner.cpp index fe6acb69b72..8418c3fce45 100644 --- a/examples/portable/executor_runner/executor_runner.cpp +++ b/examples/portable/executor_runner/executor_runner.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,14 @@ DEFINE_string( "Model serialized in flatbuffer format."); DEFINE_string(data_path, "", "Path to data file (.ptd)."); DEFINE_string(inputs, "", "Comma-separated list of input files"); +DEFINE_string( + input_shapes, + "", + "Comma-separated per-input shapes, dims joined by 'x' (e.g. " + "\"1x98,1x50x256,1x1x98\"). Inputs are resized to these shapes before " + "execute. This is the only way to run a model with dynamic shapes below " + "its upper bound from this runner; without it every execution uses the " + "bound. Input files are still upper-bound sized, the surplus is ignored."); DEFINE_string( output_file, "", @@ -300,6 +309,54 @@ bool is_expected_vgf_dump_stop(Error status) { #endif } +namespace { + +/** + * Resize the method's inputs to the shapes given by --input_shapes. + * + * Without this, a model exported with dynamic shapes can only ever be run at + * its upper bound, because prepare_input_tensors() sizes every input from the + * bound recorded in the method metadata. Several classes of backend bug are + * invisible at the bound and only appear below it, so being able to ask for a + * smaller shape matters for debugging. + */ +void resize_inputs_to(Method& method, const std::string& shapes_spec) { + std::stringstream shape_list(shapes_spec); + std::string spec; + size_t input_idx = 0; + while (std::getline(shape_list, spec, ',')) { + std::vector sizes; + std::stringstream dim_list(spec); + std::string dim; + while (std::getline(dim_list, dim, 'x')) { + sizes.push_back( + static_cast(std::stoi(dim))); + } + ET_CHECK_MSG( + input_idx < method.inputs_size(), + "--input_shapes lists more entries than the method has inputs (%zu)", + method.inputs_size()); + const auto& input = method.get_input(input_idx); + ET_CHECK_MSG( + input.isTensor(), "input %zu is not a tensor", input_idx); + auto tensor = input.toTensor(); + const Error status = executorch::ET_RUNTIME_NAMESPACE::resize_tensor( + tensor, + executorch::aten::ArrayRef( + sizes.data(), sizes.size())); + ET_CHECK_MSG( + status == Error::Ok, + "Resizing input %zu to %s failed with 0x%" PRIx32, + input_idx, + spec.c_str(), + static_cast(status)); + ++input_idx; + } + ET_LOG(Info, "Inputs resized to %s", shapes_spec.c_str()); +} + +} // namespace + int main(int argc, char** argv) { executorch::runtime::runtime_init(); @@ -673,6 +730,9 @@ int main(int argc, char** argv) { (uint32_t)res.error()); inputs.emplace(std::move(res.get())); ET_LOG(Debug, "Inputs prepared."); + if (!FLAGS_input_shapes.empty()) { + resize_inputs_to(*method, FLAGS_input_shapes); + } } Error status = method->execute(); @@ -754,6 +814,9 @@ int main(int argc, char** argv) { (uint32_t)res.error()); inputs.emplace(std::move(res.get())); ET_LOG(Debug, "Inputs prepared."); + if (!FLAGS_input_shapes.empty()) { + resize_inputs_to(*method, FLAGS_input_shapes); + } } const et_timestamp_t before_execute =