Skip to content

Commit 1b4968f

Browse files
authored
Extend PyBundledModule with extension.BundledModule
Differential Revision: D78938344 Pull Request resolved: #12839
1 parent ecb639a commit 1b4968f

File tree

8 files changed

+158
-126
lines changed

8 files changed

+158
-126
lines changed

CMakeLists.txt

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ if(MAX_KERNEL_NUM)
423423
)
424424
endif()
425425

426+
# Build devtools first if needed - some backends depend on protobuf from
427+
# devtools
428+
if(EXECUTORCH_BUILD_DEVTOOLS)
429+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/devtools)
430+
endif()
431+
426432
if(EXECUTORCH_BUILD_PYBIND AND APPLE)
427433
# shared version
428434
add_library(executorch_core_shared SHARED ${_executorch_core__srcs})
@@ -588,10 +594,6 @@ if(EXECUTORCH_BUILD_CORTEX_M)
588594
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/cortex_m)
589595
endif()
590596

591-
if(EXECUTORCH_BUILD_DEVTOOLS)
592-
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/devtools)
593-
endif()
594-
595597
if(EXECUTORCH_BUILD_EXTENSION_APPLE)
596598
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/apple)
597599
list(APPEND _executorch_extensions apple_extension)
@@ -756,6 +758,30 @@ if(EXECUTORCH_BUILD_PYBIND)
756758
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/devtools)
757759
endif()
758760

761+
# Create bundled_module target only for pybindings when bundled_program exists
762+
# This target has hard dependencies on devtools generated headers
763+
if(TARGET bundled_program)
764+
add_library(
765+
bundled_module STATIC
766+
${CMAKE_CURRENT_SOURCE_DIR}/extension/module/bundled_module.cpp
767+
)
768+
769+
# Ensure bundled_module waits for bundled_program's generated headers
770+
add_dependencies(bundled_module bundled_program)
771+
772+
target_link_libraries(bundled_module PRIVATE extension_data_loader)
773+
target_link_libraries(
774+
bundled_module PUBLIC extension_module_static bundled_program
775+
)
776+
777+
target_include_directories(
778+
bundled_module PUBLIC ${_common_include_directories}
779+
)
780+
target_compile_options(
781+
bundled_module PUBLIC -Wno-deprecated-declarations -fPIC
782+
)
783+
endif()
784+
759785
# find pytorch lib, to allow pybind to take at::Tensor as input/output
760786
find_package_torch()
761787
find_library(
@@ -773,6 +799,16 @@ if(EXECUTORCH_BUILD_PYBIND)
773799
torch
774800
)
775801

802+
if(EXECUTORCH_BUILD_EXTENSION_MODULE)
803+
# Always use static linking for pybindings to avoid runtime symbol
804+
# resolution issues
805+
list(APPEND _dep_libs extension_module_static)
806+
# Add bundled_module if available
807+
if(TARGET bundled_module)
808+
list(APPEND _dep_libs bundled_module)
809+
endif()
810+
endif()
811+
776812
if(EXECUTORCH_BUILD_TESTS)
777813
list(APPEND _dep_libs test_backend_compiler_lib)
778814
endif()

devtools/bundled_program/test/test_end2end.py

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,7 @@
55
# LICENSE file in the root directory of this source tree.
66

77
# flake8: noqa: F401
8-
import functools
9-
import inspect
10-
import os
11-
import random
128
import unittest
13-
from typing import Callable, Dict, Optional, Tuple, Type
14-
15-
import executorch.exir as exir
16-
17-
import executorch.exir.control_flow as control_flow
18-
19-
# @manual=//executorch/extension/pytree:pybindings
20-
import executorch.extension.pytree as pytree
21-
22-
import torch
239

2410
from executorch.devtools.bundled_program.core import BundledProgram
2511
from executorch.devtools.bundled_program.serialize import (
@@ -35,8 +21,6 @@
3521
try:
3622
from executorch.extension.pybindings.portable_lib import (
3723
_load_bundled_program_from_buffer,
38-
_load_for_executorch_from_buffer,
39-
_load_for_executorch_from_bundled_program,
4024
)
4125

4226
kernel_mode = "lean"
@@ -47,8 +31,6 @@
4731
try:
4832
from executorch.extension.pybindings.aten_lib import ( # @manual=//executorch/extension/pybindings:aten_lib
4933
_load_bundled_program_from_buffer,
50-
_load_for_executorch_from_buffer,
51-
_load_for_executorch_from_bundled_program,
5234
)
5335

5436
assert kernel_mode is None
@@ -75,19 +57,8 @@ def test_sample_model_e2e(self):
7557
bundled_program_buffer
7658
)
7759

78-
executorch_module = _load_for_executorch_from_bundled_program(
79-
executorch_bundled_program
80-
)
81-
8260
for method_name in eager_model.method_names:
83-
executorch_module.load_bundled_input(
84-
executorch_bundled_program,
85-
method_name,
86-
0,
87-
)
88-
executorch_module.plan_execute(method_name)
89-
executorch_module.verify_result_with_bundled_expected_output(
90-
executorch_bundled_program,
61+
executorch_bundled_program.verify_result_with_bundled_expected_output(
9162
method_name,
9263
0,
9364
)

extension/pybindings/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ CMAKE_ARGS="-DEXECUTORCH_BUILD_MPS=ON" ./install_executorch.sh
2727
- `_reset_profile_results()`: Reset profile results.
2828
## Classes
2929
### ExecuTorchModule
30-
- `load_bundled_input()`: Load bundled input.
31-
- `verify_result_with_bundled_expected_output(bundle: str, method_name: str, testset_idx: int, rtol: float = 1e-5, atol: float = 1e-8)`: Verify result with bundled expected output.
3230
- `plan_execute()`: Plan and execute.
3331
- `run_method()`: Run method.
3432
- `forward()`: Forward. This takes a pytree-flattend PyTorch-tensor-based input.
@@ -37,5 +35,6 @@ CMAKE_ARGS="-DEXECUTORCH_BUILD_MPS=ON" ./install_executorch.sh
3735
- `__call__()`: Call method.
3836
### BundledModule
3937
This class is currently empty and serves as a placeholder for future methods and attributes.
38+
- `verify_result_with_bundled_expected_output(method_name: str, testset_idx: int, rtol: float = 1e-5, atol: float = 1e-8)`: Verify result with bundled expected output.
4039
## Note
4140
All functions and methods are guarded by a call guard that redirects `cout` and `cerr` to the Python environment.

extension/pybindings/pybindings.cpp

Lines changed: 82 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <executorch/extension/data_loader/buffer_data_loader.h>
2424
#include <executorch/extension/data_loader/mmap_data_loader.h>
2525
#include <executorch/extension/memory_allocator/malloc_memory_allocator.h>
26+
#include <executorch/extension/module/bundled_module.h>
2627
#include <executorch/extension/threadpool/threadpool.h>
2728
#include <executorch/runtime/backend/interface.h>
2829
#include <executorch/runtime/core/data_loader.h>
@@ -81,6 +82,7 @@ using ::executorch::ET_RUNTIME_NAMESPACE::Program;
8182
using ::executorch::extension::BufferDataLoader;
8283
using ::executorch::extension::MallocMemoryAllocator;
8384
using ::executorch::extension::MmapDataLoader;
85+
using ::executorch::extension::ET_BUNDLED_MODULE_NAMESPACE::BundledModule;
8486
using ::executorch::runtime::ArrayRef;
8587
using ::executorch::runtime::DataLoader;
8688
using ::executorch::runtime::Error;
@@ -425,13 +427,54 @@ inline std::unique_ptr<Module> load_module_from_file(
425427
program_verification);
426428
}
427429

430+
inline py::list get_outputs_as_py_list(
431+
const std::vector<EValue>& outputs,
432+
bool clone_outputs = true) {
433+
const auto outputs_size = outputs.size();
434+
py::list list(outputs_size);
435+
for (size_t i = 0; i < outputs_size; ++i) {
436+
auto& v = outputs[i];
437+
if (Tag::None == v.tag) {
438+
list[i] = py::none();
439+
} else if (Tag::Int == v.tag) {
440+
list[i] = py::cast(v.toInt());
441+
} else if (Tag::Double == v.tag) {
442+
list[i] = py::cast(v.toDouble());
443+
} else if (Tag::Bool == v.tag) {
444+
list[i] = py::cast(v.toBool());
445+
} else if (Tag::String == v.tag) {
446+
list[i] = py::cast(std::string(v.toString().data()));
447+
} else if (Tag::Tensor == v.tag) {
448+
#ifdef USE_ATEN_LIB
449+
// Clone so the outputs in python do not share a lifetime with the
450+
// module object
451+
if (clone_outputs) {
452+
list[i] = py::cast(v.toTensor().clone());
453+
} else {
454+
list[i] = py::cast(v.toTensor());
455+
}
456+
#else
457+
if (clone_outputs) {
458+
list[i] = py::cast(alias_attensor_to_etensor(v.toTensor()).clone());
459+
} else {
460+
list[i] = py::cast(alias_attensor_to_etensor(v.toTensor()));
461+
}
462+
#endif
463+
} else {
464+
ET_ASSERT_UNREACHABLE_MSG("Invalid model output type");
465+
}
466+
}
467+
return list;
468+
}
469+
428470
static constexpr size_t kDEFAULT_BUNDLED_INPUT_POOL_SIZE = 16 * 1024U;
429471

430-
struct PyBundledModule final {
472+
struct PyBundledModule : public BundledModule {
431473
explicit PyBundledModule(
432474
const py::bytes& buffer,
433475
uint32_t bundled_input_pool_size)
434-
: bundled_program_ptr_(buffer),
476+
: BundledModule(buffer.cast<std::string_view>().data()),
477+
bundled_program_ptr_(buffer),
435478
program_ptr_(static_cast<const void*>(
436479
bundled_program_flatbuffer::GetBundledProgram(
437480
get_bundled_program_ptr())
@@ -460,6 +503,33 @@ struct PyBundledModule final {
460503
return program_len_;
461504
}
462505

506+
py::list verify_result_with_bundled_expected_output(
507+
const std::string& method_name,
508+
size_t testset_idx,
509+
double rtol = 1e-5,
510+
double atol = 1e-8) {
511+
// Execute the method
512+
auto result = BundledModule::execute(method_name, testset_idx);
513+
if (!result.ok()) {
514+
THROW_IF_ERROR(
515+
result.error(),
516+
"Method execution failed with status 0x%" PRIx32,
517+
static_cast<uint32_t>(result.error()));
518+
}
519+
520+
// Convert outputs to py::list
521+
const auto& outputs = result.get();
522+
py::list py_outputs = get_outputs_as_py_list(outputs);
523+
524+
Error status = BundledModule::verify_method_outputs(
525+
method_name, testset_idx, rtol, atol);
526+
THROW_IF_ERROR(
527+
status,
528+
"Result verification failed with status %" PRIu32,
529+
static_cast<uint32_t>(status));
530+
return py_outputs;
531+
}
532+
463533
private:
464534
// Store the bytes object instead of a raw pointer so that this module will
465535
// keep the bytes alive.
@@ -853,43 +923,6 @@ struct PyModule final {
853923
}
854924
}
855925

856-
void load_bundled_input(
857-
PyBundledModule& m,
858-
const std::string method_name,
859-
size_t testset_idx) {
860-
const void* bundled_program_ptr = m.get_bundled_program_ptr();
861-
Error status = executorch::BUNDLED_PROGRAM_NAMESPACE::load_bundled_input(
862-
module_->get_method(method_name), bundled_program_ptr, testset_idx);
863-
THROW_IF_ERROR(
864-
status,
865-
"load_bundled_input failed with status 0x%" PRIx32,
866-
static_cast<uint32_t>(status));
867-
}
868-
869-
py::list verify_result_with_bundled_expected_output(
870-
PyBundledModule& m,
871-
const std::string method_name,
872-
size_t testset_idx,
873-
double rtol = 1e-5,
874-
double atol = 1e-8) {
875-
const void* bundled_program_ptr = m.get_bundled_program_ptr();
876-
auto& method = module_->get_method(method_name);
877-
Error status = executorch::BUNDLED_PROGRAM_NAMESPACE::load_bundled_input(
878-
method, bundled_program_ptr, testset_idx);
879-
THROW_IF_ERROR(
880-
status,
881-
"load_bundled_input failed with status 0x%" PRIx32,
882-
static_cast<uint32_t>(status));
883-
py::list outputs = plan_execute(method_name);
884-
status = executorch::BUNDLED_PROGRAM_NAMESPACE::verify_method_outputs(
885-
method, bundled_program_ptr, testset_idx, rtol, atol);
886-
THROW_IF_ERROR(
887-
status,
888-
"Result verification failed with status %" PRIu32,
889-
static_cast<uint32_t>(status));
890-
return outputs;
891-
}
892-
893926
py::list plan_execute(
894927
const std::string method_name,
895928
bool clone_outputs = true) {
@@ -912,46 +945,6 @@ struct PyModule final {
912945
return get_outputs_as_py_list(outputs, clone_outputs);
913946
}
914947

915-
py::list get_outputs_as_py_list(
916-
const std::vector<EValue>& outputs,
917-
bool clone_outputs = true) {
918-
const auto outputs_size = outputs.size();
919-
py::list list(outputs_size);
920-
for (size_t i = 0; i < outputs_size; ++i) {
921-
auto& v = outputs[i];
922-
if (Tag::None == v.tag) {
923-
list[i] = py::none();
924-
} else if (Tag::Int == v.tag) {
925-
list[i] = py::cast(v.toInt());
926-
} else if (Tag::Double == v.tag) {
927-
list[i] = py::cast(v.toDouble());
928-
} else if (Tag::Bool == v.tag) {
929-
list[i] = py::cast(v.toBool());
930-
} else if (Tag::String == v.tag) {
931-
list[i] = py::cast(std::string(v.toString().data()));
932-
} else if (Tag::Tensor == v.tag) {
933-
#ifdef USE_ATEN_LIB
934-
// Clone so the outputs in python do not share a lifetime with the
935-
// module object
936-
if (clone_outputs) {
937-
list[i] = py::cast(v.toTensor().clone());
938-
} else {
939-
list[i] = py::cast(v.toTensor());
940-
}
941-
#else
942-
if (clone_outputs) {
943-
list[i] = py::cast(alias_attensor_to_etensor(v.toTensor()).clone());
944-
} else {
945-
list[i] = py::cast(alias_attensor_to_etensor(v.toTensor()));
946-
}
947-
#endif
948-
} else {
949-
ET_ASSERT_UNREACHABLE_MSG("Invalid model output type");
950-
}
951-
}
952-
return list;
953-
}
954-
955948
std::unique_ptr<PyMethodMeta> method_meta(const std::string method_name) {
956949
auto& method = module_->get_method(method_name);
957950
return std::make_unique<PyMethodMeta>(module_, method.method_meta());
@@ -1583,16 +1576,6 @@ PYBIND11_MODULE(EXECUTORCH_PYTHON_MODULE_NAME, m) {
15831576
call_guard);
15841577

15851578
py::class_<PyModule>(m, "ExecuTorchModule")
1586-
.def("load_bundled_input", &PyModule::load_bundled_input, call_guard)
1587-
.def(
1588-
"verify_result_with_bundled_expected_output",
1589-
&PyModule::verify_result_with_bundled_expected_output,
1590-
py::arg("bundle"),
1591-
py::arg("method_name"),
1592-
py::arg("testset_idx"),
1593-
py::arg("rtol") = 1e-5,
1594-
py::arg("atol") = 1e-8,
1595-
call_guard)
15961579
.def(
15971580
"plan_execute",
15981581
&PyModule::plan_execute,
@@ -1638,7 +1621,16 @@ PYBIND11_MODULE(EXECUTORCH_PYTHON_MODULE_NAME, m) {
16381621
py::arg("clone_outputs") = true,
16391622
call_guard);
16401623

1641-
py::class_<PyBundledModule>(m, "BundledModule");
1624+
py::class_<PyBundledModule>(m, "BundledModule")
1625+
.def(
1626+
"verify_result_with_bundled_expected_output",
1627+
&PyBundledModule::verify_result_with_bundled_expected_output,
1628+
py::arg("method_name"),
1629+
py::arg("testset_idx"),
1630+
py::arg("rtol") = 1e-5,
1631+
py::arg("atol") = 1e-8,
1632+
call_guard);
1633+
16421634
py::class_<PyTensorInfo>(m, "TensorInfo")
16431635
.def("sizes", &PyTensorInfo::sizes, call_guard)
16441636
.def("dtype", &PyTensorInfo::dtype, call_guard)

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ def run(self): # noqa C901
731731
cmake_build_args += ["--target", "portable_lib"]
732732
cmake_build_args += ["--target", "selective_build"]
733733

734+
if cmake_cache.is_enabled("EXECUTORCH_BUILD_EXTENSION_MODULE"):
735+
cmake_build_args += ["--target", "extension_module"]
736+
734737
if cmake_cache.is_enabled("EXECUTORCH_BUILD_EXTENSION_TRAINING"):
735738
cmake_build_args += ["--target", "_training_lib"]
736739

0 commit comments

Comments
 (0)