Skip to content

Commit a996b54

Browse files
committed
Add TensorRT weight streaming support to the ExecuTorch delegate
Apply a TensorRT weight streaming budget in the delegate init(), after the engine is deserialized and before the execution context is created (the budget cannot be changed once a context exists). When the engine was built with weight streaming, the delegate applies TensorRT's automatic budget by default, mirroring the PyTorch runtimes. An explicit budget can be set two ways, in order of precedence: a load-time ExecuTorch backend option ("weight_streaming_budget" runtime spec passed to Module::load), or the same key baked into the .pte at export via torch_tensorrt.save(output_format="executorch", weight_streaming_budget=N). The load-time option lets a deployment size the budget for its own GPU without re-exporting; the export-time value is the default and the only channel for loaders that cannot pass backend options yet (Python, Android). The value is a non-negative decimal byte count, string-encoded on the wire. Non-streamable engines make no budget call, so existing programs are unchanged. The one intended behavior change is that a streamable engine now applies the automatic budget on load, which enables running models whose weights exceed GPU memory. Ref #4334
1 parent 258a7e2 commit a996b54

11 files changed

Lines changed: 679 additions & 32 deletions

File tree

cpp/BUILD

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ cc_library(
9696
strip_include_prefix = "include",
9797
)
9898

99+
cc_library(
100+
name = "tensorrt_executorch_weight_streaming_budget",
101+
srcs = [
102+
"src/torch_tensorrt/executorch/WeightStreamingBudget.cpp",
103+
],
104+
hdrs = [
105+
"include/torch_tensorrt/executorch/WeightStreamingBudget.h",
106+
],
107+
strip_include_prefix = "include",
108+
)
109+
99110
cc_library(
100111
name = "tensorrt_executorch_binding_names",
101112
hdrs = [
@@ -127,6 +138,7 @@ cc_library(
127138
deps = [
128139
":tensorrt_executorch_binding_names",
129140
":tensorrt_executorch_blob_header",
141+
":tensorrt_executorch_weight_streaming_budget",
130142
] + select({
131143
":linux_x86_64": [
132144
"@executorch//:executorch_headers",
@@ -150,6 +162,7 @@ filegroup(
150162
"src/torch_tensorrt/executorch/README.md",
151163
"src/torch_tensorrt/executorch/TensorRTBackend.cpp",
152164
"src/torch_tensorrt/executorch/TensorRTBlobHeader.cpp",
165+
"src/torch_tensorrt/executorch/WeightStreamingBudget.cpp",
153166
],
154167
)
155168

@@ -169,5 +182,6 @@ filegroup(
169182
"include/torch_tensorrt/executorch/TensorRTBackend.h",
170183
"include/torch_tensorrt/executorch/TensorRTBindingNames.h",
171184
"include/torch_tensorrt/executorch/TensorRTBlobHeader.h",
185+
"include/torch_tensorrt/executorch/WeightStreamingBudget.h",
172186
],
173187
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
6+
namespace torch_tensorrt {
7+
namespace executorch_backend {
8+
9+
// Compile spec key that carries the weight streaming budget from export into the
10+
// delegate. Must match WEIGHT_STREAMING_BUDGET_COMPILE_SPEC_KEY on the Python
11+
// side (py/torch_tensorrt/executorch/partitioner.py).
12+
inline constexpr char kWeightStreamingBudgetKey[] = "weight_streaming_budget";
13+
14+
// Result of parsing a weight streaming budget compile spec value.
15+
//
16+
// The value is a non-negative decimal integer: an explicit GPU budget in bytes.
17+
// The automatic budget is intentionally not encoded here: when no budget spec is
18+
// present, the delegate applies TensorRT's automatic budget itself, mirroring the
19+
// PyTorch runtimes.
20+
//
21+
// valid == false -> a value was present but could not be parsed (or was
22+
// negative); the caller should reject the program.
23+
// valid == true -> bytes holds the parsed non-negative byte budget.
24+
struct WsBudget {
25+
bool valid = false;
26+
int64_t bytes = 0;
27+
};
28+
29+
// Parses a budget from a raw byte range. The value is NOT assumed to be NUL
30+
// terminated; only the first nbytes bytes are read.
31+
WsBudget parse_weight_streaming_budget(const void* value, std::size_t nbytes);
32+
33+
} // namespace executorch_backend
34+
} // namespace torch_tensorrt

cpp/src/torch_tensorrt/executorch/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ find_package(Threads REQUIRED)
2323
set(_torchtrt_executorch_sources
2424
"${CMAKE_CURRENT_LIST_DIR}/TensorRTBackend.cpp"
2525
"${CMAKE_CURRENT_LIST_DIR}/TensorRTBlobHeader.cpp"
26+
"${CMAKE_CURRENT_LIST_DIR}/WeightStreamingBudget.cpp"
2627
)
2728

2829
add_library(executorch_trt_backend STATIC ${_torchtrt_executorch_sources})

cpp/src/torch_tensorrt/executorch/TensorRTBackend.cpp

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "torch_tensorrt/executorch/TensorRTBackend.h"
99
#include "torch_tensorrt/executorch/TensorRTBindingNames.h"
1010
#include "torch_tensorrt/executorch/TensorRTBlobHeader.h"
11+
#include "torch_tensorrt/executorch/WeightStreamingBudget.h"
1112

1213
#include <cstdint>
1314
#include <cstring>
@@ -225,8 +226,6 @@ Result<DelegateHandle*> TensorRTBackend::init(
225226
BackendInitContext& context,
226227
FreeableBuffer* processed,
227228
ArrayRef<CompileSpec> compile_specs) const {
228-
(void)compile_specs;
229-
230229
TORCHTRT_ET_CHECK_NOT_NULL(processed, Error::InvalidArgument, "TensorRTBackend::init: null processed buffer");
231230
TORCHTRT_ET_CHECK_NOT_NULL(processed->data(), Error::InvalidArgument, "TensorRTBackend::init: null processed buffer");
232231

@@ -284,6 +283,130 @@ Result<DelegateHandle*> TensorRTBackend::init(
284283
TORCHTRT_ET_CHECK_NOT_NULL(
285284
handle->engine, Error::InvalidProgram, "TensorRTBackend::init: failed to deserialize TensorRT engine");
286285

286+
// Apply the weight streaming budget before the execution context is created
287+
// below: TensorRT forbids changing the budget while a context is active. The
288+
// budget is a non-negative decimal byte count and may come from two places, in
289+
// order of precedence:
290+
// 1. A load-time backend option ("weight_streaming_budget" runtime spec) that
291+
// the caller passes to Module::load(LoadBackendOptionsMap). This lets a
292+
// deployment size the budget for its own GPU without re-exporting.
293+
// 2. The same key baked into the .pte as a compile spec at export, used as a
294+
// default when no load-time option is given (and the only channel for
295+
// loaders that cannot pass backend options yet, e.g. Python/Android).
296+
// When neither is present and the engine supports streaming, we apply
297+
// TensorRT's automatic budget, mirroring what the PyTorch runtimes do on
298+
// deserialize. Negative or malformed values are rejected as InvalidProgram.
299+
WsBudget ws_request;
300+
bool is_explicit = false;
301+
302+
// (1) A load-time runtime spec takes precedence over the baked compile spec.
303+
// The value is a decimal byte string; a non-negative int is also accepted for
304+
// small budgets. A present-but-wrong-type or empty option is handled explicitly
305+
// so a runtime option is never silently dropped. The const char* returned by
306+
// get_runtime_spec points into the caller's LoadBackendOptionsMap storage, which
307+
// outlives init(); we parse it immediately and keep only the int64 result.
308+
const auto ws_runtime = context.get_runtime_spec<const char*>(kWeightStreamingBudgetKey);
309+
if (ws_runtime.ok()) {
310+
const char* const value = ws_runtime.get();
311+
// The option array need not be NUL terminated (the struct is public), so
312+
// bound the scan. An empty value means "unset", so fall through to (2).
313+
constexpr std::size_t kRuntimeBudgetMaxScan = 256;
314+
std::size_t len = 0;
315+
if (value != nullptr) {
316+
while (len < kRuntimeBudgetMaxScan && value[len] != '\0') {
317+
++len;
318+
}
319+
}
320+
if (len > 0) {
321+
ws_request = parse_weight_streaming_budget(value, len);
322+
if (!ws_request.valid) {
323+
ET_LOG(Error, "TensorRTBackend::init: malformed weight_streaming_budget runtime option");
324+
return Error::InvalidProgram;
325+
}
326+
is_explicit = true;
327+
}
328+
} else if (ws_runtime.error() != Error::NotFound) {
329+
// The key is present but stored as a non-string type. Accept a non-negative
330+
// int for convenience (its 32-bit range only covers budgets under 2 GB);
331+
// otherwise reject it so a wrong-typed option is never silently ignored.
332+
const auto ws_runtime_int = context.get_runtime_spec<int>(kWeightStreamingBudgetKey);
333+
if (ws_runtime_int.ok() && ws_runtime_int.get() >= 0) {
334+
ws_request.valid = true;
335+
ws_request.bytes = ws_runtime_int.get();
336+
is_explicit = true;
337+
} else {
338+
ET_LOG(
339+
Error,
340+
"TensorRTBackend::init: weight_streaming_budget runtime option must be a "
341+
"non-negative int or a decimal byte string");
342+
return Error::InvalidProgram;
343+
}
344+
}
345+
346+
// (2) Otherwise fall back to the compile spec baked into the .pte at export.
347+
if (!is_explicit) {
348+
const CompileSpec* ws_spec = nullptr;
349+
for (const auto& spec : compile_specs) {
350+
if (spec.key != nullptr && std::strcmp(spec.key, kWeightStreamingBudgetKey) == 0) {
351+
if (ws_spec != nullptr) {
352+
// The budget must appear at most once; a second match means the spec
353+
// list is inconsistent, so reject the program instead of guessing.
354+
ET_LOG(Error, "TensorRTBackend::init: duplicate weight_streaming_budget compile spec");
355+
return Error::InvalidProgram;
356+
}
357+
ws_spec = &spec;
358+
}
359+
}
360+
if (ws_spec != nullptr) {
361+
ws_request = parse_weight_streaming_budget(ws_spec->value.buffer, ws_spec->value.nbytes);
362+
if (!ws_request.valid) {
363+
ET_LOG(Error, "TensorRTBackend::init: malformed weight_streaming_budget compile spec");
364+
return Error::InvalidProgram;
365+
}
366+
is_explicit = true;
367+
}
368+
}
369+
370+
const int64_t streamable = handle->engine->getStreamableWeightsSize();
371+
if (streamable > 0) {
372+
// getStreamableWeightsSize is > 0 only when the engine was built with
373+
// BuilderFlag::kWEIGHT_STREAMING.
374+
int64_t budget;
375+
if (is_explicit) {
376+
// An explicit budget is a non-negative byte count, clamped to the
377+
// streamable size (TensorRT also caps it, but clamp for a clear log).
378+
budget = ws_request.bytes > streamable ? streamable : ws_request.bytes;
379+
} else {
380+
budget = handle->engine->getWeightStreamingAutomaticBudget();
381+
}
382+
if (!handle->engine->setWeightStreamingBudgetV2(budget)) {
383+
if (!is_explicit && handle->engine->setWeightStreamingBudgetV2(0)) {
384+
// The automatic budget could not be applied; fall back to budget 0, which
385+
// streams all weights (minimum resident memory) and always fits.
386+
ET_LOG(Info, "TensorRTBackend::init: automatic weight streaming budget failed; falling back to budget 0 (stream all weights)");
387+
} else {
388+
ET_LOG(
389+
Error,
390+
"TensorRTBackend::init: setWeightStreamingBudgetV2 failed (requested=%lld%s)",
391+
(long long)budget,
392+
is_explicit ? "" : ", and fallback to 0 also failed");
393+
return Error::InvalidProgram;
394+
}
395+
}
396+
ET_LOG(
397+
Info,
398+
"TensorRTBackend::init: weight streaming budget=%lld streamable=%lld scratch=%lld",
399+
(long long)handle->engine->getWeightStreamingBudgetV2(),
400+
(long long)streamable,
401+
(long long)handle->engine->getWeightStreamingScratchMemorySize());
402+
} else if (is_explicit) {
403+
// A budget was requested but the engine has no streamable weights (it was not
404+
// built with enable_weight_streaming=True, or nothing is streamable). The
405+
// engine is still valid and runs fully resident, so log and continue rather
406+
// than fail; failing here would break mixed multi-engine programs.
407+
ET_LOG(Info, "TensorRTBackend::init: weight_streaming_budget ignored; engine does not support weight streaming (build with enable_weight_streaming=True)");
408+
}
409+
287410
Error err = initialize_engine_io(*handle);
288411
if (err != Error::Ok) {
289412
return err;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include "torch_tensorrt/executorch/WeightStreamingBudget.h"
2+
3+
#include <charconv>
4+
#include <cstddef>
5+
#include <cstdint>
6+
#include <system_error>
7+
8+
namespace torch_tensorrt {
9+
namespace executorch_backend {
10+
11+
WsBudget parse_weight_streaming_budget(const void* value, std::size_t nbytes) {
12+
WsBudget result; // valid == false until the value is fully parsed
13+
14+
if (value == nullptr || nbytes == 0) {
15+
return result;
16+
}
17+
// The value is a non-negative decimal byte budget and is not NUL terminated.
18+
// std::from_chars consumes only ASCII digits (no leading whitespace, sign, or
19+
// base prefix), so a leftover byte (trailing garbage or an embedded NUL), an
20+
// out-of-range value, or a negative leaves the result invalid.
21+
const char* const first = static_cast<const char*>(value);
22+
const char* const last = first + nbytes;
23+
int64_t parsed = 0;
24+
const std::from_chars_result fc = std::from_chars(first, last, parsed);
25+
if (fc.ec != std::errc() || fc.ptr != last || parsed < 0) {
26+
return result;
27+
}
28+
29+
result.valid = true;
30+
result.bytes = parsed;
31+
return result;
32+
}
33+
34+
} // namespace executorch_backend
35+
} // namespace torch_tensorrt

0 commit comments

Comments
 (0)