Skip to content

Commit 1afd07f

Browse files
shoumikhinr
andauthored
Fix device placement for memory-planned buffers in Runtime.load_program (pytorch#22058)
# Fix device placement for memory-planned buffers in Runtime.load_program Replaces pytorch#22057. That pull request was force-pushed to a commit with no history in common with main, which made GitHub close it permanently. Same branch, same change, correct history. ## The problem ExecuTorch has two ways to load a model from Python. With the CUDA backend, one of them works and the other crashes. Same `.pte` file, same `.ptd` weights file, same default export settings. Only the loader differs. ```python # works _load_for_executorch(pte_path, ptd_path).forward([x]) # crashes Runtime.get().load_program(pte_path, data_path=ptd_path) \ .load_method("forward").execute([x]) ``` The crash looks like this: ``` [cuda_backend.cpp:548] Tensor 0 has device_type=CUDA but its data pointer 0x... is not backed by CUDA device memory (cudaPointerGetAttributes err=0, cudaMemoryType=0). RuntimeError: method->execute() failed with error 0x12 ``` ## Why it happens A `.pte` file records where each memory-planned buffer has to live, on the host or on an accelerator. That is the `non_const_buffer_device` field in the plan. `ProgramMemory` in `extension/pybindings/pybindings.cpp` never read that field. It allocated every planned buffer as host memory (`std::vector<uint8_t>`). So a program that asked for device memory received a host pointer. The CUDA backend checked the pointer, saw it was not device memory, and refused to run. ``` .pte says: buffer 0 -> CUDA:0 before: buffer 0 -> std::vector<uint8_t> (host) -> backend rejects after: buffer 0 -> DeviceMemoryBuffer::create (device) -> backend accepts ``` ## The fix `extension/module/module.cpp` hits the same problem in the C++ `Module` API and answers it differently, because it has a `share_memory_arenas` flag and this loader has none. `Module` refuses to load a device-planned method when that flag is set, and otherwise builds per-method arenas for every method. `Runtime.load_program` shares arenas unconditionally today and offers no way to turn that off, so refusing would stop files loading that load now. It keeps the shared host arenas for host methods and gives a device-planned method its own. 1. `ProgramMemory` now receives the per-buffer device list alongside the sizes, and allocates each buffer on the device it is tagged for. Host-tagged buffers keep using `std::vector<uint8_t>`, exactly as before. 2. Buffer indices are plan local. Buffer 0 of one method has nothing to do with buffer 0 of another, so one set of arenas shared by index cannot describe a file where one method plans onto the host and another onto an accelerator. A method with any device-tagged buffer therefore gets its own arenas, and every other method keeps using the shared host arenas. 3. Those arenas are built in `load_method`, not when the program is loaded. A file containing one accelerator method still opens on a machine without that accelerator, still lists its methods, and its host methods still load and run. Only loading the accelerator method fails, and it fails naming the buffer and the device it could not allocate. 4. The caller reads the device for each buffer through `MethodMeta::memory_planned_buffer_device`. 5. Two deprecated calls were replaced with their current names. Both are plain forwarders, so behavior is unchanged. `Program.load_method` in `runtime/__init__.py` documents all of this, including the part that is not new: two host-only methods of the same program share one set of arenas and therefore overwrite each other's intermediate values. ## Existing programs are unaffected `non_const_buffer_device` is optional. `MethodMeta::memory_planned_buffer_device` returns `Device{CPU, 0}` when the field is absent, which is the case for CPU-only programs and for `.pte` files produced before the field existed. Such a program keeps the shared arenas, the host allocation path, and the single argument `HierarchicalAllocator`, so `MemoryManager::has_device_memory()` stays false for it as that constructor documents. ## Test plan Two tests in `extension/pybindings/test/test_pybindings.py`. **`test_program_loads_when_one_method_is_device_planned`** covers the refusal path and needs no GPU, so it runs in the existing CPU-only job. It exports a two method program where one method has a device-tagged planned buffer and the other has none, then checks that the program loads, that the host method runs, that loading the device method reaches the device allocator and is refused there, and that the host method still runs afterwards. It first asserts that the exported program really does carry a CUDA-tagged planned buffer, so it cannot quietly degrade into a plain multi-method test if planning stops tagging devices. It skips itself on a build that links the CUDA backend, because that registers a CUDA allocator at static init, the registry has no way to drop one, and the request would then be satisfied with or without this change. **`test_device_planned_method_allocates_on_the_device`** covers what the refusal is protecting: on a build that does have a device allocator, the arena has to come off the device rather than out of host memory. It builds a two method program where one method is lowered to the CUDA backend and the other is not, then measures free device memory with `torch.cuda.mem_get_info` around each `load_method` call. It asserts that loading the host method takes no device memory, that loading the device method takes at least 90 percent of the planned device bytes, that both methods return correct numbers, that running the host method in between does not disturb the device method, and that the memory is returned once the methods and the program are all dropped. The test deletes both methods before releasing the program: each method owns its own memory, so a method kept alive after the program is released still holds its allocation. It skips unless the build links the CUDA backend and a device is visible. That second test can only run where a device allocator is registered, so this pull request also wires it into the job that has one. The `unittest-cuda` job in `.github/workflows/cuda.yml` runs it, right after the install that job already does and before its builds, since the test needs nothing they produce. That workflow now triggers on changes under `extension/pybindings/` and to `runtime/__init__.py`, and the job condition lists the same two paths so the job actually fires on them. Before this, no job in the repository built the Python extension with a device allocator and then ran the pybindings tests, which is exactly why this class of defect was invisible. ### Measured Linux x86_64, NVIDIA A100 80GB, compute capability 8.0, CUDA 13.0, Python 3.12, torch 2.13.0. Two builds from one source tree, one CPU only and one with the CUDA backend. The before column is the merge base with `main`, produced by swapping only `pybindings.cpp` and rebuilding, so both columns are the same machine, the same model files and the same everything else. Two methods in one program, `forward` on the host and `forward2` lowered to CUDA, planned device bytes 50331648 (48 MiB): | Measurement | Before | After | | --- | --- | --- | | Device memory taken by `load_program` | 0 MiB | 0 MiB | | Device memory taken by loading the host method | 0 MiB | 0 MiB | | Device memory taken by loading the CUDA method | 0 MiB | 48 MiB | | CUDA method produces correct numbers | no, error 0x12 | yes | | Host method produces correct numbers | yes | yes | | CUDA method still correct after running the host method | not reached | yes | | Device memory returned once the methods and program are dropped | nothing to return | 48 MiB | The before column is not a generic failure. The CUDA backend names the defect itself: ``` [cuda_backend.cpp:548] Tensor 0 has device_type=CUDA but its data pointer 0x7f5842fff010 is not backed by CUDA device memory (cudaPointerGetAttributes err=0, cudaMemoryType=0). [method.cpp:1528] CALL_DELEGATE execute failed at instruction 2: 0x12 ``` Suites, on the same two builds: | Suite | CUDA build | CPU-only build | | --- | --- | --- | | `extension/pybindings/test/test_pybindings.py` | 39 passed, 1 skipped, 2 failed | 39 passed, 1 skipped, 2 failed | | the same file filtered to `-k device` | 4 passed, refusal test skipped | 4 passed, success test skipped | | `test_device_planned_method_allocates_on_the_device` alone, run the way the CI script runs it | passed | skipped | The two failures are `test_method_quantized_ops` and `test_quantized_ops`. They are pre-existing and unrelated: they need the quantized AOT library preloaded, which the Buck target does and a bare `pytest` invocation does not. They reproduce identically at the merge base. Linux aarch64, Jetson Orin Nano, Python 3.10, CPU-only build: identical counts to the x86_64 CPU-only column above, including the device tests and the same two pre-existing quantized-op failures. The CUDA success path is not reachable on that board, because its GPU needs a PyTorch build pinned to a different version than this repository requires, so only the host paths and the refusal path are covered there. ## Landing order This should land after or together with pytorch#22095. The device arenas added here are allocated through the device allocator, and ETDump can be handed pointers into them when a delegate logs its arguments. `BufferDataSink::write` does a plain host `memcpy`, so recording a CUDA tensor would read device memory from the host. pytorch#22095 is the fix for that path: it routes non-CPU tensors through a device copy before writing. Landing this one first leaves that combination reachable whenever event tracing is on. ## Not covered - Leaving a device-planned method out of the shared host arenas is not covered by any test. Delete that skip and both tests still pass, because nothing in Python can observe the shared arena sizes. What it saves is host memory that nothing reads, which grows with the model, so it is worth a C++ test later. - The device path is measured on one accelerator, an A100 with compute capability 8.0. Not measured on a Jetson board or on any non-CUDA accelerator. - `has_device_buffers` and `make_method_memory` ask `MethodMeta` for one buffer at a time, and `MethodMeta::memory_planned_buffer_device` scans the sparse device list on each call, so the cost is the buffer count times the device entry count. Real programs measured here have 2 or 3 buffers and 1 device entry, and `extension/module/module.cpp` already reads the same metadata the same way, but both counts come from the file. Removing the concern properly means a bulk accessor on `MethodMeta`, which would fix both callers at once and belongs in its own change. --------- Co-authored-by: Anthony Shoumikhin <shoumikhin@users.noreply.github.com> Co-authored-by: r <r@e>
1 parent 7d163c9 commit 1afd07f

7 files changed

Lines changed: 399 additions & 21 deletions

File tree

‎.github/workflows/cuda.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ on:
2626
- .ci/scripts/export_model_artifact.sh
2727
- .ci/scripts/test_model_e2e.sh
2828
- examples/models/muse-glimmer/**
29+
- extension/pybindings/**
30+
- runtime/__init__.py
2931
workflow_dispatch:
3032

3133
concurrency:
@@ -160,13 +162,17 @@ jobs:
160162
unittest-cuda:
161163
name: unittest-cuda
162164
needs: [changed-files, run-decision]
165+
# This job runs the pybindings device test, so it also has to fire on the
166+
# two pybindings paths the workflow filter lists.
163167
if: |
164168
contains(needs.changed-files.outputs.changed-files, 'backends/cuda') ||
165169
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
166170
contains(needs.changed-files.outputs.changed-files, '.github/workflows/cuda.yml') ||
167171
contains(needs.changed-files.outputs.changed-files, '.ci/scripts/test-cuda-build.sh') ||
168172
contains(needs.changed-files.outputs.changed-files, '.ci/scripts/export_model_artifact.sh') ||
169173
contains(needs.changed-files.outputs.changed-files, '.ci/scripts/test_model_e2e.sh') ||
174+
contains(needs.changed-files.outputs.changed-files, 'extension/pybindings') ||
175+
contains(needs.changed-files.outputs.changed-files, 'runtime/__init__.py') ||
170176
needs.run-decision.outputs.is-full-run == 'true'
171177
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
172178
permissions:
@@ -190,6 +196,29 @@ jobs:
190196
conda install -y -c conda-forge 'libstdcxx-ng>=12'
191197
export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH
192198
199+
# The pybindings loader only asks for device memory on a build that has
200+
# a device allocator registered, so this test cannot run in the CPU
201+
# jobs. It needs the install above and nothing built below, so run it
202+
# here and fail before the long builds.
203+
#
204+
# unittest exits 0 when a test skips, and this test skips when the CUDA
205+
# backend is missing or no GPU is visible. Both are the prerequisites
206+
# this job exists to provide, so treat a skip as a failure rather than
207+
# let a packaging regression pass silently. `tee` would otherwise hide
208+
# the real exit status behind its own, so read it back explicitly.
209+
set +e
210+
python -m unittest -v executorch.extension.pybindings.test.test_pybindings.PybindingsTest.test_device_planned_method_allocates_on_the_device 2>&1 | tee /tmp/pybindings_device_test.log
211+
pybindings_device_status=${PIPESTATUS[0]}
212+
set -e
213+
if [ "${pybindings_device_status}" -ne 0 ]; then
214+
echo "::error::the pybindings device test failed"
215+
exit "${pybindings_device_status}"
216+
fi
217+
if grep -qE "^OK \(skipped=|skipped=[1-9]" /tmp/pybindings_device_test.log; then
218+
echo "::error::the pybindings device test skipped in the CUDA job, which means the CUDA backend or a visible GPU is missing"
219+
exit 1
220+
fi
221+
193222
# Build ExecuTorch with CUDA support
194223
cmake --workflow --preset llm-release-cuda
195224

‎extension/pybindings/pybindings.cpp‎

Lines changed: 179 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
#include <algorithm>
10+
#include <cinttypes>
1011
#include <cstdio>
1112
#include <iostream>
1213
#include <memory>
@@ -31,6 +32,7 @@
3132
#include <executorch/extension/threadpool/threadpool.h>
3233
#include <executorch/runtime/backend/interface.h>
3334
#include <executorch/runtime/core/data_loader.h>
35+
#include <executorch/runtime/core/device_memory_buffer.h>
3436
#include <executorch/runtime/core/exec_aten/util/scalar_type_util.h>
3537
#include <executorch/runtime/executor/method.h>
3638
#include <executorch/runtime/executor/program.h>
@@ -81,13 +83,15 @@ using ::executorch::ET_RUNTIME_NAMESPACE::get_num_registered_backends;
8183
using ::executorch::ET_RUNTIME_NAMESPACE::get_registered_kernels;
8284
using ::executorch::ET_RUNTIME_NAMESPACE::Kernel;
8385
using ::executorch::ET_RUNTIME_NAMESPACE::Method;
86+
using ::executorch::ET_RUNTIME_NAMESPACE::MethodMeta;
8487
using ::executorch::ET_RUNTIME_NAMESPACE::Program;
8588
using ::executorch::extension::BufferDataLoader;
8689
using ::executorch::extension::MallocMemoryAllocator;
8790
using ::executorch::extension::MmapDataLoader;
8891
using ::executorch::extension::ET_BUNDLED_MODULE_NAMESPACE::BundledModule;
8992
using ::executorch::extension::pybindings::PyDataLoader;
9093
using ::executorch::runtime::DataLoader;
94+
using ::executorch::runtime::DeviceMemoryBuffer;
9195
using ::executorch::runtime::Error;
9296
using ::executorch::runtime::EValue;
9397
using ::executorch::runtime::EventTracerDebugLogLevel;
@@ -1077,18 +1081,33 @@ inline std::shared_ptr<ProgramState> load_program(
10771081
/// A wrapper/util class for executorch memory allocations/manager.
10781082
class ProgramMemory {
10791083
public:
1080-
explicit ProgramMemory(std::vector<std::vector<uint8_t>>&& non_const_buffers)
1084+
/// `devices` is empty when every buffer is on the host, which keeps
1085+
/// `MemoryManager::has_device_memory()` false for CPU-only programs.
1086+
/// Otherwise it holds one entry per buffer, indexed like `sizes`.
1087+
///
1088+
/// Members initialize in declaration order and each one reads the members
1089+
/// declared before it, so that order is load-bearing. Device buffers come
1090+
/// first so that a device that is missing or out of memory throws before the
1091+
/// host arenas are allocated and zero-filled, rather than after.
1092+
ProgramMemory(
1093+
std::vector<int64_t>&& sizes,
1094+
std::vector<runtime::etensor::Device>&& devices)
10811095
: runtime_allocator_(),
1082-
non_const_buffers_(std::move(non_const_buffers)),
1096+
planned_sizes_(std::move(sizes)),
1097+
planned_devices_(std::move(devices)),
1098+
device_buffers_(allocate_device_buffers()),
1099+
non_const_buffers_(allocate_host_buffers()),
10831100
non_const_spans_(create_non_const_spans()),
1084-
non_const_allocator_(
1085-
{non_const_spans_.data(), non_const_spans_.size()}),
1101+
non_const_allocator_(create_non_const_allocator()),
10861102
mem_manager_(
10871103
&const_allocator_,
10881104
&non_const_allocator_,
10891105
&runtime_allocator_,
10901106
&temp_allocator_) {}
10911107

1108+
explicit ProgramMemory(std::vector<int64_t>&& sizes)
1109+
: ProgramMemory(std::move(sizes), {}) {}
1110+
10921111
/// Returns a pointer to the internal memory manager, the Memory instance
10931112
/// must outlive this pointer.
10941113
MemoryManager* mem_manager() {
@@ -1105,6 +1124,16 @@ class ProgramMemory {
11051124

11061125
MallocMemoryAllocator temp_allocator_{};
11071126

1127+
std::vector<int64_t> planned_sizes_;
1128+
1129+
std::vector<runtime::etensor::Device> planned_devices_;
1130+
1131+
// Backs device-tagged buffers; the entry is empty for a CPU-tagged buffer.
1132+
// Parallel to non_const_buffers_ so both index by planned buffer id. Empty
1133+
// for an all-host program.
1134+
std::vector<DeviceMemoryBuffer> device_buffers_;
1135+
1136+
// Backs CPU-tagged buffers; the entry is empty for a device-tagged buffer.
11081137
std::vector<std::vector<uint8_t>> non_const_buffers_;
11091138

11101139
std::vector<Span<uint8_t>> non_const_spans_;
@@ -1113,16 +1142,125 @@ class ProgramMemory {
11131142

11141143
MemoryManager mem_manager_;
11151144

1145+
bool is_device_buffer(size_t index) const {
1146+
return index < planned_devices_.size() && !planned_devices_[index].is_cpu();
1147+
}
1148+
1149+
std::vector<std::vector<uint8_t>> allocate_host_buffers() {
1150+
std::vector<std::vector<uint8_t>> result;
1151+
result.reserve(planned_sizes_.size());
1152+
for (size_t i = 0; i < planned_sizes_.size(); ++i) {
1153+
if (is_device_buffer(i)) {
1154+
result.emplace_back();
1155+
} else {
1156+
result.emplace_back(planned_sizes_[i]);
1157+
}
1158+
}
1159+
return result;
1160+
}
1161+
1162+
std::vector<DeviceMemoryBuffer> allocate_device_buffers() {
1163+
std::vector<DeviceMemoryBuffer> result;
1164+
if (planned_devices_.empty()) {
1165+
return result;
1166+
}
1167+
// Both vectors are filled in lockstep today, so this only fires if a
1168+
// future caller breaks that. HierarchicalAllocator aborts on a mismatch,
1169+
// so check here instead, where a Python caller can catch it.
1170+
THROW_IF_ERROR(
1171+
planned_devices_.size() == planned_sizes_.size()
1172+
? Error::Ok
1173+
: Error::InvalidArgument,
1174+
"Have %zu planned buffer sizes but %zu device tags",
1175+
planned_sizes_.size(),
1176+
planned_devices_.size());
1177+
result.reserve(planned_sizes_.size());
1178+
for (size_t i = 0; i < planned_sizes_.size(); ++i) {
1179+
if (!is_device_buffer(i)) {
1180+
result.emplace_back();
1181+
continue;
1182+
}
1183+
auto buffer = DeviceMemoryBuffer::create(
1184+
planned_sizes_[i],
1185+
planned_devices_[i].type(),
1186+
planned_devices_[i].index());
1187+
THROW_IF_ERROR(
1188+
buffer.error(),
1189+
"Failed to allocate %" PRId64 " bytes for buffer %zu on device %d:%d",
1190+
planned_sizes_[i],
1191+
i,
1192+
static_cast<int>(planned_devices_[i].type()),
1193+
static_cast<int>(planned_devices_[i].index()));
1194+
result.emplace_back(std::move(buffer.get()));
1195+
}
1196+
return result;
1197+
}
1198+
11161199
std::vector<Span<uint8_t>> create_non_const_spans() {
11171200
std::vector<Span<uint8_t>> result;
1118-
for (size_t i = 0; i < non_const_buffers_.size(); i++) {
1119-
result.push_back(
1120-
{non_const_buffers_[i].data(), non_const_buffers_[i].size()});
1201+
result.reserve(planned_sizes_.size());
1202+
for (size_t i = 0; i < planned_sizes_.size(); ++i) {
1203+
if (is_device_buffer(i)) {
1204+
result.push_back(device_buffers_[i].as_span());
1205+
} else {
1206+
result.push_back(
1207+
{non_const_buffers_[i].data(), non_const_buffers_[i].size()});
1208+
}
11211209
}
11221210
return result;
11231211
}
1212+
1213+
HierarchicalAllocator create_non_const_allocator() {
1214+
Span<Span<uint8_t>> buffers(
1215+
non_const_spans_.data(), non_const_spans_.size());
1216+
return planned_devices_.empty()
1217+
? HierarchicalAllocator(buffers)
1218+
: HierarchicalAllocator(
1219+
buffers, {planned_devices_.data(), planned_devices_.size()});
1220+
}
11241221
};
11251222

1223+
/// True if any of the method's memory-planned buffers must live off the host.
1224+
bool has_device_buffers(const MethodMeta& method_meta) {
1225+
for (size_t i = 0; i < method_meta.num_memory_planned_buffers(); ++i) {
1226+
auto device = method_meta.memory_planned_buffer_device(i);
1227+
THROW_IF_ERROR(
1228+
device.error(), "Failed to get device of planned buffer %zu", i);
1229+
if (!device.get().is_cpu()) {
1230+
return true;
1231+
}
1232+
}
1233+
return false;
1234+
}
1235+
1236+
/// Arenas sized and placed for a single method, used when that method's
1237+
/// buffers cannot come from the program-wide host arenas. Returns nullptr when
1238+
/// every buffer is on the host, so one pass over the metadata answers both
1239+
/// whether the method needs its own arenas and how big they are.
1240+
std::shared_ptr<ProgramMemory> make_method_memory(
1241+
const MethodMeta& method_meta) {
1242+
const size_t num_buffers = method_meta.num_memory_planned_buffers();
1243+
std::vector<int64_t> sizes;
1244+
std::vector<runtime::etensor::Device> devices;
1245+
sizes.reserve(num_buffers);
1246+
devices.reserve(num_buffers);
1247+
bool needs_device_memory = false;
1248+
for (size_t i = 0; i < num_buffers; ++i) {
1249+
auto size = method_meta.memory_planned_buffer_size(i);
1250+
THROW_IF_ERROR(size.error(), "Failed to get size of planned buffer %zu", i);
1251+
auto device = method_meta.memory_planned_buffer_device(i);
1252+
THROW_IF_ERROR(
1253+
device.error(), "Failed to get device of planned buffer %zu", i);
1254+
needs_device_memory |= !device.get().is_cpu();
1255+
sizes.push_back(size.get());
1256+
devices.push_back(device.get());
1257+
}
1258+
if (!needs_device_memory) {
1259+
return nullptr;
1260+
}
1261+
return std::make_shared<ProgramMemory>(std::move(sizes), std::move(devices));
1262+
}
1263+
11261264
struct PyMethod final {
11271265
explicit PyMethod(
11281266
std::shared_ptr<ProgramMemory> memory,
@@ -1423,8 +1561,17 @@ struct PyProgram final {
14231561
for (size_t i = 0; i < state_->program_->num_methods(); ++i) {
14241562
auto name = state_->program_->get_method_name(i).get();
14251563
auto method_meta = state_->program_->method_meta(name).get();
1426-
for (size_t j = 0; j < method_meta.num_non_const_buffers(); j++) {
1427-
int64_t buffer_size = method_meta.non_const_buffer_size(j).get();
1564+
// A device-planned method gets its own arenas in load_method and never
1565+
// reads these, so letting its sizes in would only grow the host arenas
1566+
// the other methods share.
1567+
if (has_device_buffers(method_meta)) {
1568+
continue;
1569+
}
1570+
for (size_t j = 0; j < method_meta.num_memory_planned_buffers(); ++j) {
1571+
auto size = method_meta.memory_planned_buffer_size(j);
1572+
THROW_IF_ERROR(
1573+
size.error(), "Failed to get size of planned buffer %zu", j);
1574+
int64_t buffer_size = size.get();
14281575
if (non_const_buffer_sizes.find(j) == non_const_buffer_sizes.end()) {
14291576
non_const_buffer_sizes.insert({j, buffer_size});
14301577
} else {
@@ -1434,16 +1581,14 @@ struct PyProgram final {
14341581
}
14351582
}
14361583

1437-
// Allocate the arenas. Using vector because we need to remember the size as
1438-
// well, so vector is easier then unique_ptr.
1439-
std::vector<std::vector<uint8_t>> non_const_buffers_;
1440-
for (std::map<size_t, int64_t>::iterator i = non_const_buffer_sizes.begin();
1441-
i != non_const_buffer_sizes.end();
1442-
i++) {
1443-
non_const_buffers_.push_back(std::vector<uint8_t>(i->second));
1584+
// Allocate the shared host arenas.
1585+
std::vector<int64_t> planned_sizes;
1586+
planned_sizes.reserve(non_const_buffer_sizes.size());
1587+
for (const auto& entry : non_const_buffer_sizes) {
1588+
planned_sizes.push_back(entry.second);
14441589
}
14451590

1446-
memory_ = std::make_shared<ProgramMemory>(std::move(non_const_buffers_));
1591+
memory_ = std::make_shared<ProgramMemory>(std::move(planned_sizes));
14471592
if (event_tracer_ && debug_buffer_size > 0) {
14481593
// If a debug buffer was requested for the ETDump, allocate it and make
14491594
// sure its lifetime is as long as the event_tracer.
@@ -1508,9 +1653,22 @@ struct PyProgram final {
15081653
}
15091654

15101655
std::unique_ptr<PyMethod> load_method(const std::string& method_name) {
1656+
Result<MethodMeta> meta =
1657+
state_->program_->method_meta(method_name.c_str());
1658+
THROW_IF_ERROR(
1659+
meta.error(),
1660+
"Failed to get method meta for method %s, error: 0x:%" PRIx32,
1661+
method_name.c_str(),
1662+
static_cast<uint32_t>(meta.error()));
1663+
// Device memory is claimed here rather than at program load so that one
1664+
// accelerator method cannot make the rest of the program unloadable. A
1665+
// host-only method keeps sharing the program-wide arenas, so its planned
1666+
// memory is not isolated from the other host-only methods of this program.
1667+
auto method_memory = make_method_memory(meta.get());
1668+
auto memory = method_memory ? std::move(method_memory) : memory_;
15111669
Result<Method> res = state_->program_->load_method(
15121670
method_name.c_str(),
1513-
memory_->mem_manager(),
1671+
memory->mem_manager(),
15141672
event_tracer_.get(),
15151673
state_->data_map_.get());
15161674
THROW_IF_ERROR(
@@ -1519,7 +1677,9 @@ struct PyProgram final {
15191677
method_name.c_str(),
15201678
static_cast<uint32_t>(res.error()));
15211679
return std::make_unique<PyMethod>(
1522-
memory_, state_, std::make_unique<Method>(std::move(res.get())));
1680+
std::move(memory),
1681+
state_,
1682+
std::make_unique<Method>(std::move(res.get())));
15231683
}
15241684

15251685
Span<uint8_t> get_etdump_debug_buffer() {

‎extension/pybindings/test/BUCK‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fbcode_target(
2626
"//executorch/exir:pass_manager",
2727
"//executorch/exir:scalar_type",
2828
"//executorch/exir/_serialize:lib",
29+
"//executorch/exir/backend:partitioner",
2930
"//executorch/exir/emit:lib",
3031
"//executorch/exir/passes:lib",
3132
"//executorch/runtime/core:core",
@@ -39,6 +40,8 @@ fbcode_target(
3940
preload_deps = ["//executorch/kernels/quantized:aot_lib"],
4041
deps = [
4142
":make_test",
43+
"//executorch/exir:schema",
44+
"//executorch/exir/backend/test:device_util",
4245
"//executorch/extension/pybindings:portable_lib",
4346
],
4447
)
@@ -50,6 +53,8 @@ fbcode_target(
5053
preload_deps = ["//executorch/kernels/quantized:aot_lib"],
5154
deps = [
5255
":make_test",
56+
"//executorch/exir:schema",
57+
"//executorch/exir/backend/test:device_util",
5358
"//executorch/extension/pybindings:aten_lib",
5459
"//executorch/kernels/quantized:aot_lib",
5560
],
@@ -61,6 +66,8 @@ fbcode_target(
6166
srcs = ["test_pybindings.py"],
6267
deps = [
6368
":make_test",
69+
"//executorch/exir:schema",
70+
"//executorch/exir/backend/test:device_util",
6471
],
6572
)
6673

0 commit comments

Comments
 (0)