This is an automated email from the ASF dual-hosted git repository.

tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 84f3dad2a9 [Relax][TensorRT] Build and embed engines during code 
generation (#20301)
84f3dad2a9 is described below

commit 84f3dad2a94f4dcf793a43e158a68c3713741ce6
Author: Zupeng Wang <[email protected]>
AuthorDate: Fri Sep 11 11:42:44 2026 +0800

    [Relax][TensorRT] Build and embed engines during code generation (#20301)
    
    TensorRT BYOC exports currently store graph metadata, so the first
    inference builds an engine unless a separate disk cache is managed. Add
    opt-in `relax.ext.tensorrt.options.build_at_compile_time=True` to build
    engines during `RunCodegen` and embed their plans in the exported
    library, addressing #20040. No warm-up inference is needed before
    export.
    
    The default lazy path retains its existing byte format. Prebuilt modules
    store a versioned TensorRT-specific trailer and deserialize on the
    execution GPU without a builder fallback. The mode supports FP32/FP16
    with positive static input dimensions; dynamic input shapes and INT8
    calibration are rejected. Plans require a compatible GPU, platform, and
    TensorRT version. Resource cleanup covers failed construction, CPU
    tensor staging, and CUDA shutdown.
    
    Validation on RTX 3090, TensorRT 10.13.3, CUDA 13.0, and LLVM 18.1.8:
    
    - New feature tests: **14 passed, 1 skipped**. Fresh subprocesses
    interpose `createInferBuilder_INTERNAL`: lazy exports hit the guard,
    while prebuilt exports produce correct outputs and exit normally. Cases
    cover weighted multi-partition graphs, FP32/FP16, GPU 1, CPU tensor
    staging, and a public-API Conv2d+ReLU partition, along with
    serialization compatibility, malformed plans, and rejected calibration
    settings.
    - Existing TensorRT codegen/inference tests: **31 passed**; backend
    partition tests: **7 passed**.
    - Separate baseline and patched builds completed; scoped pre-commit and
    patch-application checks passed. Runtime-disabled branches passed syntax
    compilation; their full integration build was not run, and the
    corresponding test was skipped.
    
    These results were obtained on base
    `40c2f54908e96875fa19e92db83f123854d97991` with tvm-ffi
    `9d784c4da74ff7360d76c79a89fe60a63516f880`. Main subsequently advanced
    through #20294 and #20299, updating tvm-ffi; the local results do not
    cover that newer integration. This Draft preserves the reviewed and
    tested patch. No startup-latency or throughput claim is made.
    
    Signed-off-by: Zupeng Wang <[email protected]>
---
 python/tvm/relax/backend/contrib/tensorrt.py       |  50 +++
 src/relax/backend/contrib/tensorrt/codegen.cc      |  54 ++-
 .../extra/contrib/tensorrt/tensorrt_builder.cc     |  56 +--
 .../extra/contrib/tensorrt/tensorrt_builder.h      |   4 +
 .../extra/contrib/tensorrt/tensorrt_runtime.cc     | 269 ++++++++++++-
 .../relax/test_tensorrt_engine_serialization.py    | 422 +++++++++++++++++++++
 6 files changed, 814 insertions(+), 41 deletions(-)

diff --git a/python/tvm/relax/backend/contrib/tensorrt.py 
b/python/tvm/relax/backend/contrib/tensorrt.py
index 21ab01b24d..3711f29de1 100644
--- a/python/tvm/relax/backend/contrib/tensorrt.py
+++ b/python/tvm/relax/backend/contrib/tensorrt.py
@@ -203,6 +203,56 @@ def partition_for_tensorrt(mod: IRModule) -> IRModule:
     mod : tvm.ir.IRModule
         The module with TensorRT-supported subgraphs grouped into composite
         functions annotated for the ``tensorrt`` codegen.
+
+    Notes
+    -----
+    By default, TensorRT builds engines on the first inference call.
+    To build and embed engines before exporting the library, enable
+    ``build_at_compile_time`` in the ``relax.ext.tensorrt.options`` PassContext
+    configuration while running ``RunCodegen``.
+
+    Compile-time building requires the TensorRT runtime and a CUDA GPU on the
+    compilation host. Select the deployment GPU before starting code 
generation,
+    for example with ``CUDA_VISIBLE_DEVICES``. Tensor inputs must have static,
+    positive dimensions and FP16 or FP32 dtype. Dynamic inputs and INT8
+    calibration are not supported by this mode.
+
+    The exported library contains the serialized engines. No inference call or
+    disk engine cache is needed before export. On the first inference call,
+    the runtime deserializes these plans instead of rebuilding engines.
+    Deployment still requires the TensorRT-enabled TVM runtime, CUDA, and a
+    compatible GPU and TensorRT version; the plans are not portable across
+    arbitrary GPU architectures or TensorRT versions.
+
+    Examples
+    --------
+    After binding model weights as constants, partition and generate code on 
the
+    compilation GPU before exporting::
+
+        import tvm
+        from tvm import relax
+        from tvm.relax.backend.contrib.tensorrt import partition_for_tensorrt
+
+        partitioned = partition_for_tensorrt(mod)
+        with tvm.transform.PassContext(
+            config={
+                "relax.ext.tensorrt.options": {
+                    "build_at_compile_time": True,
+                }
+            }
+        ):
+            offloaded = relax.transform.RunCodegen()(partitioned)
+        executable = tvm.compile(offloaded, target="cuda")
+        executable.export_library("model.so")
+
+    Load the library and run inference in a new process on the deployment GPU::
+
+        import tvm
+        from tvm import relax
+
+        executable = tvm.runtime.load_module("model.so")
+        vm = relax.VirtualMachine(executable, tvm.cuda(0))
+        result = vm["main"](*inputs)
     """
     patterns = get_patterns_with_prefix("tensorrt")
     mod = FuseOpsByPattern(patterns, bind_constants=True, 
annotate_codegen=False)(mod)
diff --git a/src/relax/backend/contrib/tensorrt/codegen.cc 
b/src/relax/backend/contrib/tensorrt/codegen.cc
index 2b5d4d3db7..4d40ec9185 100644
--- a/src/relax/backend/contrib/tensorrt/codegen.cc
+++ b/src/relax/backend/contrib/tensorrt/codegen.cc
@@ -60,6 +60,7 @@ struct TensorRTCompilerConfigNode : public ffi::Object {
   bool remove_no_mac_subgraphs;
   bool use_fp16;
   bool use_uint8;
+  bool build_at_compile_time;
 
   static void RegisterReflection() {
     namespace refl = tvm::ffi::reflection;
@@ -77,6 +78,9 @@ struct TensorRTCompilerConfigNode : public ffi::Object {
         .def_ro("use_fp16", &TensorRTCompilerConfigNode::use_fp16, "Use FP16",
                 refl::DefaultValue(false))
         .def_ro("use_uint8", &TensorRTCompilerConfigNode::use_uint8, "Use 
uint8",
+                refl::DefaultValue(false))
+        .def_ro("build_at_compile_time", 
&TensorRTCompilerConfigNode::build_at_compile_time,
+                "Build and embed TensorRT engines during code generation",
                 refl::DefaultValue(false));
   }
   TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.ext.attrs.TensorRTCompilerConfig",
@@ -220,8 +224,8 @@ class CollectFromCompositeFunctionBody : public ExprVisitor 
{
 
 /*!
  * \brief Generates an TensorRTModule from a relax expression by serializing 
the expression to a
- * json representation. TensorRT is not required here because use of TensorRT 
APIs is deferred until
- * runtime.
+ * json representation. TensorRT APIs are deferred until runtime unless 
compile-time engine
+ * building is explicitly enabled.
  */
 class TensorRTJSONSerializer : public JSONSerializer {
  public:
@@ -335,18 +339,60 @@ void CollectFromCompositeFunctionBody::VisitExpr_(const 
CallNode* call_node) {
 ffi::Array<ffi::Module> TensorRTCompiler(ffi::Array<Function> functions,
                                          ffi::Map<ffi::String, ffi::Any> 
/*unused*/,
                                          ffi::Map<Constant, ffi::String> 
constant_names) {
+  auto cfg = 
transform::PassContext::Current()->GetConfig<TensorRTCompilerConfig>(
+      "relax.ext.tensorrt.options");
+  bool build_at_compile_time = cfg.has_value() && 
cfg.value()->build_at_compile_time;
+  ffi::Map<ffi::String, runtime::Tensor> constant_tensors;
+  if (build_at_compile_time) {
+    for (const auto& entry : constant_names) {
+      constant_tensors.Set(entry.second, entry.first->data);
+    }
+  }
+
   ffi::Array<ffi::Module> compiled_functions;
   for (const auto& func : functions) {
     VLOG(1) << "TensorRT partition:" << std::endl << func;
+    if (build_at_compile_time) {
+      // Reject dynamic inputs before the JSON serializer requires integer 
shapes.
+      auto check_input_type = [&](const auto& self, const Type& type) -> void {
+        if (const auto* tuple = type.as<TupleTypeNode>()) {
+          for (const auto& field : tuple->fields) self(self, field);
+          return;
+        }
+        const auto* tensor = type.as<TensorTypeNode>();
+        TVM_FFI_CHECK(tensor != nullptr && tensor->shape.has_value(), 
ValueError)
+            << "TensorRT compile-time engine building requires static positive 
input dimensions";
+        const auto* shape = tensor->shape.value().as<ShapeExprNode>();
+        TVM_FFI_CHECK(shape != nullptr, ValueError)
+            << "TensorRT compile-time engine building requires static positive 
input dimensions";
+        for (const auto& dim : shape->values) {
+          const auto* value = dim.as<IntImmNode>();
+          TVM_FFI_CHECK(value != nullptr && value->value > 0, ValueError)
+              << "TensorRT compile-time engine building requires static 
positive input dimensions";
+        }
+      };
+      for (const auto& param : func->params) 
check_input_type(check_input_type, GetType(param));
+    }
     TensorRTJSONSerializer serializer(constant_names, AnalyzeVar2Value(func));
     serializer.serialize(func);
     std::string graph_json = serializer.GetJSON();
     VLOG(1) << "TensorRT JSON:" << std::endl << graph_json;
-    auto constant_names = serializer.GetConstantNames();
+    auto ordered_constant_names = serializer.GetConstantNames();
     const auto pf = 
tvm::ffi::Function::GetGlobalRequired("runtime.tensorrt_runtime_create");
     std::string func_name = GetExtSymbol(func);
     VLOG(1) << "Creating tensorrt ffi::Module for '" << func_name << "'";
-    compiled_functions.push_back(pf(func_name, graph_json, 
constant_names).cast<ffi::Module>());
+    auto module = pf(func_name, graph_json, 
ordered_constant_names).cast<ffi::Module>();
+    if (build_at_compile_time) {
+      auto build_engine = module->GetFunction("build_engine");
+      TVM_FFI_CHECK(build_engine.has_value(), RuntimeError)
+          << "TensorRT compile-time engine building requires the TensorRT 
runtime";
+      ffi::Array<runtime::Tensor> ordered_constants;
+      for (const auto& name : ordered_constant_names) {
+        ordered_constants.push_back(constant_tensors[name]);
+      }
+      build_engine.value()(ordered_constants);
+    }
+    compiled_functions.push_back(module);
   }
   return compiled_functions;
 }
diff --git a/src/runtime/extra/contrib/tensorrt/tensorrt_builder.cc 
b/src/runtime/extra/contrib/tensorrt/tensorrt_builder.cc
index 10b3bdf447..94a03dabcc 100644
--- a/src/runtime/extra/contrib/tensorrt/tensorrt_builder.cc
+++ b/src/runtime/extra/contrib/tensorrt/tensorrt_builder.cc
@@ -49,7 +49,8 @@ TensorRTBuilder::TensorRTBuilder(TensorRTLogger* logger,
       use_int8_(false),
       calibrator_(calibrator) {
   // Create TRT builder and network.
-  builder_ = nvinfer1::createInferBuilder(*trt_logger_);
+  std::unique_ptr<nvinfer1::IBuilder> 
builder(nvinfer1::createInferBuilder(*trt_logger_));
+  TVM_FFI_ICHECK(builder != nullptr) << "Creating the TensorRT builder failed";
 
   // TensorRT 10 removed implicit-batch mode and the kEXPLICIT_BATCH creation 
flag; every network is
   // explicit-batch, so the batch dimension is simply dimension 0 of each 
binding and is varied
@@ -57,9 +58,14 @@ TensorRTBuilder::TensorRTBuilder(TensorRTLogger* logger,
   if (calibrator_ != nullptr) {
     use_int8_ = true;
   }
-  network_ = builder_->createNetworkV2(0U);
+  std::unique_ptr<nvinfer1::INetworkDefinition> 
network(builder->createNetworkV2(0U));
+  TVM_FFI_ICHECK(network != nullptr) << "Creating the TensorRT network failed";
+  builder_ = builder.release();
+  network_ = network.release();
 }
 
+TensorRTBuilder::~TensorRTBuilder() { CleanUp(); }
+
 nvinfer1::DataType DLDataType2NVDataType(DLDataType data_type) {
   TVM_FFI_ICHECK(data_type.code == kDLFloat && (data_type.bits == 16 || 
data_type.bits == 32))
       << "Invalid input Tensor type. Only float16 and float32 are supported";
@@ -155,6 +161,7 @@ void TensorRTBuilder::AddLayer(int nid, const 
JSONGraphNode& node) {
 TensorRTEngineAndContext TensorRTBuilder::BuildEngine() {
   // Build engine.
   config_ = builder_->createBuilderConfig();
+  TVM_FFI_ICHECK(config_ != nullptr) << "Creating the TensorRT builder config 
failed";
   // TensorRT 10 replaced IBuilderConfig::setMaxWorkspaceSize with a tunable 
memory pool.
   config_->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 
max_workspace_size_);
   // Disable TF32 (on by default on Ampere+) so FP32 layers match TVM's 
full-precision reference.
@@ -172,7 +179,9 @@ TensorRTEngineAndContext TensorRTBuilder::BuildEngine() {
 
   // Every network is explicit-batch in TRT10, so always add an optimization 
profile that pins each
   // input to its concrete shape (with a minimum batch of 1 for dynamic batch 
dimensions).
+  // The builder owns this profile and releases it during CleanUp.
   auto profile = builder_->createOptimizationProfile();
+  TVM_FFI_ICHECK(profile != nullptr) << "Creating the TensorRT optimization 
profile failed";
   for (int i = 0; i < network_->getNbInputs(); ++i) {
     auto name = network_->getInput(i)->getName();
     const uint32_t entry_id = entry_id_map_[name];
@@ -194,23 +203,27 @@ TensorRTEngineAndContext TensorRTBuilder::BuildEngine() {
 
   // TensorRT 10 removed buildEngineWithConfig; build a serialized engine and 
deserialize it through
   // an IRuntime that is kept alive alongside the engine 
(TensorRTEngineAndContext::runtime).
-  nvinfer1::IHostMemory* plan = builder_->buildSerializedNetwork(*network_, 
*config_);
-  TVM_FFI_ICHECK(plan) << "Failed to build TensorRT serialized network.";
-  nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(*trt_logger_);
-  nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(plan->data(), 
plan->size());
-  delete plan;
-  if (engine == nullptr) {
-    delete runtime;
-    TVM_FFI_THROW(InternalError) << "Failed to deserialize the TensorRT 
engine.";
-  }
+  std::unique_ptr<nvinfer1::IHostMemory> plan(
+      builder_->buildSerializedNetwork(*network_, *config_));
+  TVM_FFI_ICHECK(plan != nullptr) << "Failed to build TensorRT serialized 
network.";
+  std::unique_ptr<nvinfer1::IRuntime> 
runtime(nvinfer1::createInferRuntime(*trt_logger_));
+  TVM_FFI_ICHECK(runtime != nullptr) << "Creating the TensorRT deserialization 
runtime failed";
+  std::unique_ptr<nvinfer1::ICudaEngine> engine(
+      runtime->deserializeCudaEngine(plan->data(), plan->size()));
+  TVM_FFI_ICHECK(engine != nullptr) << "Failed to deserialize the TensorRT 
engine.";
   TVM_FFI_ICHECK_EQ(engine->getNbIOTensors(), 
static_cast<int32_t>(network_input_names_.size() +
                                                                    
network_output_names_.size()));
-  nvinfer1::IExecutionContext* context = engine->createExecutionContext();
+  std::unique_ptr<nvinfer1::IExecutionContext> 
context(engine->createExecutionContext());
+  TVM_FFI_ICHECK(context != nullptr) << "Creating the TensorRT execution 
context failed";
   CleanUp();
 
-  TVM_FFI_ICHECK(context);
-
-  return {runtime, engine, context, network_input_names_, 
network_output_names_};
+  TensorRTEngineAndContext result;
+  result.inputs = network_input_names_;
+  result.outputs = network_output_names_;
+  result.runtime = runtime.release();
+  result.engine = engine.release();
+  result.context = context.release();
+  return result;
 }
 
 nvinfer1::Weights TensorRTBuilder::GetDLTensorAsWeights(const DLTensor* dptr,
@@ -228,11 +241,12 @@ nvinfer1::Weights 
TensorRTBuilder::GetDLTensorAsWeights(const DLTensor* dptr,
     count *= dptr->shape[i];
   }
   weight.count = count;
-  weight.values = new float[count];
-  // Tensor::CopyToBytes throws on failure (the old C API 
TVMTensorCopyToBytes/TVMGetLastError
-  // were removed during the tvm-ffi refactor).
-  Tensor::CopyToBytes(dptr, const_cast<void*>(weight.values), weight_bytes);
+  std::unique_ptr<float[]> values(new float[count]);
+  weight.values = values.get();
+  // Keep temporary storage owned until both the copy and registration succeed.
+  Tensor::CopyToBytes(dptr, values.get(), weight_bytes);
   trt_weights_.push_back(weight);
+  values.release();
   return weight;
 }
 
@@ -246,23 +260,19 @@ nvinfer1::ITensor* 
TensorRTBuilder::GetInputAsTensor(const TensorRTOpInput& inpu
 void TensorRTBuilder::CleanUp() {
   // TensorRT 10 removed obj->destroy(); objects are released with the delete 
operator.
   VLOG(1) << "Destroying TensorRT network";
-  TVM_FFI_ICHECK(network_);
   delete network_;
   network_ = nullptr;
 
   VLOG(1) << "Destroying TensorRT config";
-  TVM_FFI_ICHECK(config_);
   delete config_;
   config_ = nullptr;
 
   VLOG(1) << "Destroying TensorRT builder";
-  TVM_FFI_ICHECK(builder_);
   delete builder_;
   builder_ = nullptr;
 
   VLOG(1) << "Destroying TensorRT weights";
   for (auto weight : trt_weights_) {
-    TVM_FFI_ICHECK(weight.values);
     if (weight.type == nvinfer1::DataType::kFLOAT || weight.type == 
nvinfer1::DataType::kHALF) {
       delete[] static_cast<const float*>(weight.values);
     } else {
diff --git a/src/runtime/extra/contrib/tensorrt/tensorrt_builder.h 
b/src/runtime/extra/contrib/tensorrt/tensorrt_builder.h
index 108f56b9f3..0fd06acc24 100644
--- a/src/runtime/extra/contrib/tensorrt/tensorrt_builder.h
+++ b/src/runtime/extra/contrib/tensorrt/tensorrt_builder.h
@@ -76,6 +76,10 @@ class TensorRTBuilder {
                   size_t max_workspace_size, bool use_fp16,
                   nvinfer1::IInt8Calibrator* calibrator = nullptr);
 
+  ~TensorRTBuilder();
+  TensorRTBuilder(const TensorRTBuilder&) = delete;
+  TensorRTBuilder& operator=(const TensorRTBuilder&) = delete;
+
   /*!
    * \brief Add TensorRT input(s) for input node in network definition.
    * \param nid The input node id.
diff --git a/src/runtime/extra/contrib/tensorrt/tensorrt_runtime.cc 
b/src/runtime/extra/contrib/tensorrt/tensorrt_runtime.cc
index b421e096d7..1f177f7605 100644
--- a/src/runtime/extra/contrib/tensorrt/tensorrt_runtime.cc
+++ b/src/runtime/extra/contrib/tensorrt/tensorrt_runtime.cc
@@ -28,10 +28,13 @@
 #include <tvm/runtime/logging.h>
 #include <tvm/runtime/tensor.h>
 
+#include <cstdint>
 #include <fstream>
+#include <limits>
 #include <memory>
 #include <string>
 #include <unordered_map>
+#include <utility>
 #include <vector>
 
 #include "../../../../support/env.h"
@@ -42,6 +45,7 @@
 #ifdef TVM_GRAPH_EXECUTOR_TENSORRT
 #include <tvm/ffi/extra/c_env_api.h>
 #include <tvm/ffi/extra/cuda/base.h>
+#include <tvm/ffi/extra/cuda/device_guard.h>
 
 #include "NvInfer.h"
 #include "tensorrt_builder.h"
@@ -108,6 +112,70 @@ class TensorRTRuntime : public JSONRuntimeBase {
     return ffi::Module::kBinarySerializable | ffi::Module::kRunnable;
   }
 
+  ffi::Optional<ffi::Function> GetFunction(const ffi::String& name) override {
+    if (name == "build_engine") {
+      ffi::ObjectPtr<ffi::Object> self = ffi::GetObjectPtr<ffi::Object>(this);
+      return ffi::Function([self, this](ffi::PackedArgs args, ffi::Any* rv) {
+        TVM_FFI_ICHECK_EQ(args.size(), 1U);
+        std::lock_guard<std::mutex> guard(initialize_mutex_);
+        TVM_FFI_ICHECK(!initialized_) << "Build the TensorRT engine before 
initializing the module";
+        BuildEngineAtCompileTime(args[0].cast<ffi::Array<Tensor>>());
+        *rv = 0;
+      });
+    }
+    return JSONRuntimeBase::GetFunction(name);
+  }
+
+  ffi::Bytes SaveToBytes() const override {
+    auto graph_bytes = JSONRuntimeBase::SaveToBytes();
+    if (serialized_engine_.empty()) return graph_bytes;
+
+    // Keep the JSON runtime prefix unchanged. Only opt-in, prebuilt modules 
have this trailer.
+    std::string result(graph_bytes.data(), graph_bytes.size());
+    support::BytesOutStream stream(&result);
+    stream.Write(kEngineMagic);
+    stream.Write(kEngineVersion);
+    stream.Write(serialized_engine_);
+    stream.Write(serialized_inputs_);
+    stream.Write(serialized_outputs_);
+    stream.Write(serialized_batch_size_);
+    return ffi::Bytes(std::move(result));
+  }
+
+  static ffi::Module LoadFromBytes(const ffi::Bytes& bytes) {
+    support::BytesInStream stream(bytes);
+    std::string symbol, graph_json;
+    std::vector<std::string> consts;
+    TVM_FFI_ICHECK(stream.Read(&symbol)) << "Loading symbol name failed";
+    TVM_FFI_ICHECK(stream.Read(&graph_json)) << "Loading graph json failed";
+    TVM_FFI_ICHECK(stream.Read(&consts)) << "Loading the const name list 
failed";
+    ffi::Array<ffi::String> const_names;
+    for (const auto& name : consts) const_names.push_back(name);
+    auto n = ffi::make_object<TensorRTRuntime>(symbol, graph_json, 
const_names);
+
+    uint64_t magic;
+    size_t nread = stream.Read(&magic, sizeof(magic));
+    if (nread == 0) return ffi::Module(n);  // Legacy JSON-only module.
+    TVM_FFI_ICHECK_EQ(nread, sizeof(magic)) << "Truncated TensorRT engine 
trailer";
+    TVM_FFI_ICHECK_EQ(magic, kEngineMagic) << "Invalid TensorRT engine 
trailer";
+    uint32_t version;
+    TVM_FFI_ICHECK(stream.Read(&version)) << "Loading TensorRT engine version 
failed";
+    TVM_FFI_ICHECK_EQ(version, kEngineVersion) << "Unsupported TensorRT engine 
version";
+    TVM_FFI_ICHECK(stream.Read(&n->serialized_engine_)) << "Loading TensorRT 
engine failed";
+    TVM_FFI_ICHECK(!n->serialized_engine_.empty()) << "The embedded TensorRT 
engine is empty";
+    TVM_FFI_ICHECK(stream.Read(&n->serialized_inputs_)) << "Loading TensorRT 
input names failed";
+    TVM_FFI_ICHECK(stream.Read(&n->serialized_outputs_)) << "Loading TensorRT 
output names failed";
+    TVM_FFI_ICHECK(stream.Read(&n->serialized_batch_size_)) << "Loading 
TensorRT batch size failed";
+    TVM_FFI_ICHECK_GT(n->serialized_batch_size_, 0) << "Invalid TensorRT batch 
size";
+    TVM_FFI_ICHECK_EQ(n->serialized_inputs_.size(), n->input_var_eid_.size());
+    TVM_FFI_ICHECK_EQ(n->serialized_outputs_.size(), n->outputs_.size());
+    char trailing;
+    TVM_FFI_ICHECK_EQ(stream.Read(&trailing, sizeof(trailing)), 0U)
+        << "Unexpected data after the TensorRT engine";
+    // Defer GPU-dependent deserialization until the first call supplies the 
execution device.
+    return ffi::Module(n);
+  }
+
   /*!
    * \brief Initialize runtime. Create TensorRT layer from JSON
    * representation.
@@ -117,9 +185,15 @@ class TensorRTRuntime : public JSONRuntimeBase {
   void Init(const ffi::Array<Tensor>& consts) override {
     TVM_FFI_ICHECK_EQ(consts.size(), const_idx_.size())
         << "The number of input constants must match the number of required.";
+    if (!serialized_engine_.empty()) {
+      TVM_FFI_ICHECK(!support::GetEnv("TVM_TENSORRT_USE_INT8", false) &&
+                     support::GetEnv("TENSORRT_NUM_CALI_INT8", 0) == 0 &&
+                     num_calibration_batches_remaining_ == 0)
+          << "A prebuilt TensorRT engine cannot perform INT8 calibration";
+    }
     LoadGlobalAttributes();
     SetupConstants(consts);
-    GetCachedEnginesFromDisk();
+    if (serialized_engine_.empty()) GetCachedEnginesFromDisk();
   }
 
   void LoadGlobalAttributes() {
@@ -149,8 +223,19 @@ class TensorRTRuntime : public JSONRuntimeBase {
   }
 
 #ifdef TVM_GRAPH_EXECUTOR_TENSORRT
-  /*! \brief Destroy engines and contexts. */
+  /*! \brief Destroy engines and contexts on the device that owns them. */
   void DestroyEngines() {
+    if (embedded_device_id_ >= 0) {
+      ffi::CUDADeviceGuard guard(embedded_device_id_);
+      DestroyEnginesOnCurrentDevice();
+      device_buffers_.reset();
+      embedded_device_id_ = -1;
+    } else {
+      DestroyEnginesOnCurrentDevice();
+    }
+  }
+
+  void DestroyEnginesOnCurrentDevice() {
     for (auto& it : trt_engine_cache_) {
       // TensorRT 10 removed obj->destroy(); release with delete. The 
deserialization runtime must
       // outlive the engine it produced, so delete the context, then the 
engine, then the runtime.
@@ -166,13 +251,143 @@ class TensorRTRuntime : public JSONRuntimeBase {
   }
 
   ~TensorRTRuntime() {
+    // FFI can retain imported modules until process shutdown, after CUDA has 
already unloaded.
+    // TensorRT destructors and CUDADeviceGuard cannot run then; the process 
reclaims the resources.
+    int current_device;
+    if ((!trt_engine_cache_.empty() || device_buffers_ || calibrator_) &&
+        cudaGetDevice(&current_device) == cudaErrorCudartUnloading) {
+      // CPU-input staging tensors and the calibrator also own CUDA 
allocations. Release their
+      // containers so member destruction cannot enter CUDA after this 
confirmed shutdown state.
+      (void)device_buffers_.release();
+      (void)calibrator_.release();
+      return;
+    }
     VLOG(1) << "Destroying TensorRT runtime";
     DestroyEngines();
     VLOG(1) << "Destroyed TensorRT runtime";
   }
 
-  /*! \brief Run inference using built engine. */
+  /*! \brief Build from static input metadata and weights, without running 
inference. */
+  void BuildEngineAtCompileTime(const ffi::Array<Tensor>& consts) {
+    TVM_FFI_ICHECK_EQ(consts.size(), const_idx_.size())
+        << "The number of input constants must match the number of required.";
+    TVM_FFI_ICHECK(!support::GetEnv("TVM_TENSORRT_USE_INT8", false) &&
+                   support::GetEnv("TENSORRT_NUM_CALI_INT8", 0) == 0 &&
+                   num_calibration_batches_remaining_ == 0)
+        << "TensorRT build_at_compile_time does not support INT8 calibration";
+    TVM_FFI_ICHECK(!input_var_eid_.empty())
+        << "TensorRT build_at_compile_time requires at least one tensor input";
+    TVM_FFI_ICHECK(serialized_engine_.empty()) << "The TensorRT engine has 
already been built";
+
+    std::vector<std::vector<int64_t>> shapes(NumEntries());
+    std::vector<DLTensor> inputs(NumEntries());
+    for (auto nid : input_nodes_) {
+      const auto& node = nodes_[nid];
+      if (node.GetOpType() != "input") continue;
+      auto node_shapes = node.GetOpShape();
+      auto dtypes = node.GetOpDataType();
+      for (size_t j = 0; j < node_shapes.size(); ++j) {
+        TVM_FFI_ICHECK(dtypes[j].code == kDLFloat && dtypes[j].lanes == 1 &&
+                       (dtypes[j].bits == 16 || dtypes[j].bits == 32))
+            << "TensorRT build_at_compile_time requires float16 or float32 
inputs";
+        uint32_t eid = EntryID(nid, j);
+        for (int64_t dim : node_shapes[j]) {
+          TVM_FFI_ICHECK_GT(dim, 0)
+              << "TensorRT build_at_compile_time requires static positive 
input dimensions";
+          TVM_FFI_ICHECK_LE(dim, std::numeric_limits<int32_t>::max())
+              << "TensorRT input dimensions must fit in int32";
+          shapes[eid].push_back(dim);
+        }
+        inputs[eid].device = DLDevice{kDLCPU, 0};
+        inputs[eid].ndim = static_cast<int32_t>(shapes[eid].size());
+        inputs[eid].dtype = dtypes[j];
+        inputs[eid].shape = shapes[eid].data();
+      }
+    }
+
+    for (const auto& constant : consts) {
+      TVM_FFI_ICHECK_EQ(constant->device.device_type, kDLCPU)
+          << "TensorRT build_at_compile_time requires constants on the CPU";
+      const auto dtype = constant->dtype;
+      TVM_FFI_ICHECK(dtype.code == kDLFloat && dtype.lanes == 1 &&
+                     (dtype.bits == 16 || dtype.bits == 32))
+          << "TensorRT build_at_compile_time requires float16 or float32 
constants";
+    }
+    LoadGlobalAttributes();
+    auto original_entries = data_entry_;
+    try {
+      SetupConstants(consts);
+      for (auto eid : input_var_eid_) data_entry_[eid] = &inputs[eid];
+      int batch_size = GetBatchSize();
+      BuildEngineFromJson(batch_size);
+      const auto& built = trt_engine_cache_.at(std::make_pair(symbol_name_, 
batch_size));
+      std::unique_ptr<nvinfer1::IHostMemory> plan(built.engine->serialize());
+      TVM_FFI_ICHECK(plan != nullptr) << "Serializing the TensorRT engine 
failed";
+      TVM_FFI_ICHECK_GT(plan->size(), 0U) << "The serialized TensorRT engine 
is empty";
+      serialized_engine_.assign(static_cast<const char*>(plan->data()), 
plan->size());
+      serialized_inputs_ = built.inputs;
+      serialized_outputs_ = built.outputs;
+      serialized_batch_size_ = batch_size;
+    } catch (...) {
+      data_entry_ = std::move(original_entries);
+      DestroyEngines();
+      serialized_engine_.clear();
+      serialized_inputs_.clear();
+      serialized_outputs_.clear();
+      serialized_batch_size_ = 0;
+      throw;
+    }
+    // Neither input metadata nor the supplied constants are owned by the 
runtime at this point.
+    data_entry_ = std::move(original_entries);
+    DestroyEngines();
+  }
+
+  void LoadEmbeddedEngine(int device_id) {
+    TVM_FFI_ICHECK(!support::GetEnv("TVM_TENSORRT_USE_INT8", false) &&
+                   support::GetEnv("TENSORRT_NUM_CALI_INT8", 0) == 0 &&
+                   num_calibration_batches_remaining_ == 0)
+        << "A prebuilt TensorRT engine cannot perform INT8 calibration";
+    std::unique_ptr<nvinfer1::IRuntime> 
runtime(nvinfer1::createInferRuntime(GetTensorRTLogger()));
+    TVM_FFI_ICHECK(runtime != nullptr) << "Creating the TensorRT 
deserialization runtime failed";
+    std::unique_ptr<nvinfer1::ICudaEngine> engine(
+        runtime->deserializeCudaEngine(serialized_engine_.data(), 
serialized_engine_.size()));
+    TVM_FFI_ICHECK(engine != nullptr)
+        << "Failed to deserialize the embedded TensorRT engine. Recompile the 
model for this "
+           "GPU, platform, and TensorRT version; compile-time engines are not 
rebuilt at runtime.";
+    std::unique_ptr<nvinfer1::IExecutionContext> 
context(engine->createExecutionContext());
+    TVM_FFI_ICHECK(context != nullptr) << "Creating the embedded TensorRT 
execution context failed";
+    TensorRTEngineAndContext loaded;
+    loaded.inputs = serialized_inputs_;
+    loaded.outputs = serialized_outputs_;
+    auto key = std::make_pair(symbol_name_, serialized_batch_size_);
+    auto result = trt_engine_cache_.emplace(key, std::move(loaded));
+    TVM_FFI_ICHECK(result.second) << "The embedded TensorRT engine has already 
been initialized";
+    result.first->second.runtime = runtime.release();
+    result.first->second.engine = engine.release();
+    result.first->second.context = context.release();
+    max_batch_size_ = serialized_batch_size_;
+    embedded_device_id_ = device_id;
+  }
+
+  /*! \brief Run inference using a prebuilt or lazily built engine. */
   void Run() override {
+    if (serialized_engine_.empty()) {
+      RunWithEngine();
+      return;
+    }
+    // Const-loader initialization has no execution device. The first 
invocation does, so load the
+    // plan here without invoking the TensorRT builder and restore the 
caller's current device.
+    const DLDevice& dev = data_entry_[input_var_eid_[0]]->device;
+    const int device_id = dev.device_type == kDLCUDA ? dev.device_id : 0;
+    ffi::CUDADeviceGuard guard(device_id);
+    if (embedded_device_id_ != device_id) {
+      DestroyEngines();
+      LoadEmbeddedEngine(device_id);
+    }
+    RunWithEngine();
+  }
+
+  void RunWithEngine() {
     auto& engine_and_context = GetOrBuildEngine();
     int batch_size = GetBatchSize();
     if (batch_size == 0) return;
@@ -297,6 +512,12 @@ class TensorRTRuntime : public JSONRuntimeBase {
     int batch_size = GetBatchSize();
     int compatible_engine_batch_size = -1;
     bool find_engine_flag = FindCompatibleEngine(batch_size, 
&compatible_engine_batch_size);
+    if (!serialized_engine_.empty()) {
+      TVM_FFI_ICHECK_EQ(batch_size, serialized_batch_size_)
+          << "The input batch size does not match the compile-time TensorRT 
engine";
+      TVM_FFI_ICHECK(find_engine_flag) << "The embedded TensorRT engine has 
not been initialized";
+      return trt_engine_cache_.at(std::make_pair(symbol_name_, 
serialized_batch_size_));
+    }
     const bool use_int8 = (support::GetEnv("TVM_TENSORRT_USE_INT8", 0) != 0);
     const bool int8_calibration_not_used_or_not_complete =
         (calibrator_ != nullptr && num_calibration_batches_remaining_ != 0);
@@ -478,24 +699,27 @@ class TensorRTRuntime : public JSONRuntimeBase {
   /*! \brief Retreive a GPU buffer for input or output or allocate if needed. 
Keyed by TensorRT IO
    * tensor name (TRT10 has no binding indices). */
   Tensor GetOrAllocateDeviceBuffer(const std::string& name, int entry_id) {
+    if (!device_buffers_) {
+      device_buffers_ = std::make_unique<std::unordered_map<std::string, 
Tensor>>();
+    }
+    auto& device_buffers = *device_buffers_;
+    DLDevice device{kDLCUDA, embedded_device_id_ >= 0 ? embedded_device_id_ : 
0};
     std::vector<int64_t> shape(data_entry_[entry_id]->shape,
                                data_entry_[entry_id]->shape + 
data_entry_[entry_id]->ndim);
-    if (device_buffers_.count(name)) {
+    if (device_buffers.count(name)) {
       // Buffer is already initialized.
-      if (shape[0] > device_buffers_[name]->shape[0]) {
+      if (shape[0] > device_buffers[name]->shape[0]) {
         // Buffer is too small. Need to allocate bigger buffer.
-        device_buffers_[name] =
-            runtime::Tensor::Empty(shape, data_entry_[entry_id]->dtype, 
{kDLCUDA, 0});
-      } else if (shape[0] < device_buffers_[name]->shape[0]) {
+        device_buffers[name] = runtime::Tensor::Empty(shape, 
data_entry_[entry_id]->dtype, device);
+      } else if (shape[0] < device_buffers[name]->shape[0]) {
         // Buffer is too large. Create view.
-        return device_buffers_[name].CreateView(shape, 
data_entry_[entry_id]->dtype);
+        return device_buffers[name].CreateView(shape, 
data_entry_[entry_id]->dtype);
       }
     } else {
       // Buffer not initialized yet.
-      device_buffers_[name] =
-          runtime::Tensor::Empty(shape, data_entry_[entry_id]->dtype, 
{kDLCUDA, 0});
+      device_buffers[name] = runtime::Tensor::Empty(shape, 
data_entry_[entry_id]->dtype, device);
     }
-    return device_buffers_.at(name);
+    return device_buffers.at(name);
   }
 
   void CreateInt8Calibrator(const TensorRTEngineAndContext& 
engine_and_context) {
@@ -513,6 +737,9 @@ class TensorRTRuntime : public JSONRuntimeBase {
   std::unordered_map<std::pair<std::string, int>, TensorRTEngineAndContext, 
PairHash>
       trt_engine_cache_;
 
+  /*! \brief Device owning the deserialized compile-time engine, or -1 before 
the first call. */
+  int embedded_device_id_ = -1;
+
   /*! \brief Calibrator for INT8 mode. */
   std::unique_ptr<TensorRTCalibrator> calibrator_;
 
@@ -520,7 +747,7 @@ class TensorRTRuntime : public JSONRuntimeBase {
    * is not "cuda". Since TensorRT execution can only read data from GPU, we 
need to copy data from
    * the runtime device to these buffers first. These will be allocated for 
the highest batch size
    * used by all engines. */
-  std::unordered_map<std::string, Tensor> device_buffers_;
+  std::unique_ptr<std::unordered_map<std::string, Tensor>> device_buffers_;
 
 #else   // TVM_GRAPH_EXECUTOR_TENSORRT
   void Run() override {
@@ -533,11 +760,25 @@ class TensorRTRuntime : public JSONRuntimeBase {
                  << "Please build with USE_TENSORRT_RUNTIME.";
   }
 
+  void BuildEngineAtCompileTime(const ffi::Array<Tensor>& consts) {
+    TVM_FFI_THROW(InternalError)
+        << "TensorRT build_at_compile_time requires TVM to be built with 
USE_TENSORRT_RUNTIME";
+  }
+
   bool GetCachedEnginesFromDisk() { return false; }
 
   void CacheEngineToDisk() {}
 #endif  // TVM_GRAPH_EXECUTOR_TENSORRT
 
+  static constexpr uint64_t kEngineMagic = 0x31545254564d5445ULL;
+  static constexpr uint32_t kEngineVersion = 1;
+
+  // Retain serialized bytes independently of initialized CUDA objects, 
including after reloading.
+  std::string serialized_engine_;
+  std::vector<std::string> serialized_inputs_;
+  std::vector<std::string> serialized_outputs_;
+  int serialized_batch_size_ = 0;
+
   bool use_implicit_batch_;
 
   size_t max_workspace_size_;
@@ -570,7 +811,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   refl::GlobalDef()
       .def("runtime.tensorrt_runtime_create", TensorRTRuntimeCreate)
-      .def("ffi.Module.load_from_bytes.tensorrt", 
JSONRuntimeBase::LoadFromBytes<TensorRTRuntime>);
+      .def("ffi.Module.load_from_bytes.tensorrt", 
TensorRTRuntime::LoadFromBytes);
 }
 
 }  // namespace contrib
diff --git a/tests/python/relax/test_tensorrt_engine_serialization.py 
b/tests/python/relax/test_tensorrt_engine_serialization.py
new file mode 100644
index 0000000000..a527c63880
--- /dev/null
+++ b/tests/python/relax/test_tensorrt_engine_serialization.py
@@ -0,0 +1,422 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Compile-time TensorRT engines and their exported runtime artifacts."""
+
+import json
+import os
+import struct
+import subprocess
+import sys
+import textwrap
+
+import numpy as np
+import pytest
+
+import tvm
+import tvm.testing
+from tvm import relax, tirx
+from tvm.relax.backend.contrib.tensorrt import partition_for_tensorrt
+from tvm.relax.dpl import is_op, wildcard
+from tvm.support import cc
+from tvm.testing import env
+
+_has_codegen = tvm.get_global_func("relax.ext.tensorrt", True) is not None
+_runtime_enabled = tvm.get_global_func("relax.is_tensorrt_runtime_enabled", 
True)
+_has_runtime = _runtime_enabled is not None and _runtime_enabled()
+
+pytestmark = pytest.mark.skipif(not _has_codegen, reason="TensorRT codegen is 
not enabled")
+requires_runtime = pytest.mark.skipif(not _has_runtime, reason="TensorRT 
runtime is not enabled")
+requires_gpu = pytest.mark.skipif(not env.has_cuda(), reason="CUDA is not 
available")
+
+# Version 1 extends the legacy JSON runtime prefix with a private engine 
trailer.
+_ENGINE_MAGIC = 0x31545254564D5445
+
+
[email protected](autouse=True)
+def isolated_tensorrt_options(monkeypatch):
+    for name in (
+        "TVM_TENSORRT_CACHE_DIR",
+        "TVM_TENSORRT_USE_INT8",
+        "TENSORRT_NUM_CALI_INT8",
+        "TVM_TENSORRT_MULTI_ENGINE",
+        "TVM_TENSORRT_USE_FP16",
+        "TVM_TENSORRT_MAX_WORKSPACE_SIZE",
+    ):
+        monkeypatch.delenv(name, raising=False)
+
+
+def _string(value):
+    value = value.encode() if isinstance(value, str) else value
+    return struct.pack("<Q", len(value)) + value
+
+
+def _strings(values):
+    return struct.pack("<Q", len(values)) + b"".join(_string(value) for value 
in values)
+
+
+def _legacy_bytes(module):
+    return (
+        _string(module["get_symbol"]())
+        + _string(module.inspect_source())
+        + _strings(module["get_const_vars"]())
+    )
+
+
+def _module_bytes(module):
+    """Extract a leaf's payload from the same import packer used by 
export_library."""
+    assert not module.imports
+    packed = 
tvm.get_global_func("runtime.ModulePackImportsToTensor")(module).numpy().tobytes()
+    assert struct.unpack_from("<Q", packed)[0] == len(packed) - 8
+    offset = 8
+    # The import tree consists of row pointers followed by child indices.
+    for _ in range(2):
+        count = struct.unpack_from("<Q", packed, offset)[0]
+        offset += 8 + 8 * count
+    kind_size = struct.unpack_from("<Q", packed, offset)[0]
+    offset += 8
+    assert packed[offset : offset + kind_size] == b"tensorrt"
+    offset += kind_size
+    payload_size = struct.unpack_from("<Q", packed, offset)[0]
+    offset += 8
+    assert offset + payload_size == len(packed)
+    return packed[offset:]
+
+
+def _load_bytes(payload):
+    return tvm.get_global_func("ffi.Module.load_from_bytes.tensorrt")(payload)
+
+
+def _relu_partition(shape=(2, 4)):
+    data = relax.Var("data", relax.TensorType(shape, "float32"))
+    builder = relax.BlockBuilder()
+    with builder.function("main", [data]):
+        with builder.dataflow():
+            output = builder.emit_output(relax.op.nn.relu(data))
+        builder.emit_func_output(output)
+    return tvm.transform.Sequential(
+        [
+            relax.transform.FuseOpsByPattern(
+                [("tensorrt.nn.relu", is_op("relax.nn.relu")(wildcard()))]
+            ),
+            relax.transform.MergeCompositeFunctions(),
+        ]
+    )(builder.get())
+
+
+def _codegen(partitioned, eager=None):
+    config = {}
+    if eager is not None:
+        config["relax.ext.tensorrt.options"] = {"build_at_compile_time": eager}
+    with tvm.transform.PassContext(config=config):
+        return relax.transform.RunCodegen()(partitioned)
+
+
+def _external_modules(mod):
+    modules = list(mod.attrs["external_mods"])
+    assert modules and all(module.kind == "tensorrt" for module in modules)
+    return modules
+
+
+def test_default_preserves_legacy_bytes():
+    partitioned = _relu_partition()
+    default = _external_modules(_codegen(partitioned))[0]
+    disabled = _external_modules(_codegen(partitioned, eager=False))[0]
+    payload = _legacy_bytes(default)
+    assert _module_bytes(default) == payload
+    assert _module_bytes(disabled) == payload
+    # A real old-format record must remain loadable and exportable without a 
GPU.
+    restored = _load_bytes(payload)
+    assert restored["get_symbol"]() == default["get_symbol"]()
+    assert restored.inspect_source() == default.inspect_source()
+    assert _module_bytes(restored) == payload
+
+
[email protected](_has_runtime, reason="This test requires a codegen-only 
TensorRT build")
+def test_eager_requires_runtime():
+    with pytest.raises(RuntimeError, match="USE_TENSORRT_RUNTIME"):
+        _codegen(_relu_partition(), eager=True)
+
+
[email protected](
+    "trailer, message",
+    [
+        (b"partial", "Truncated TensorRT engine trailer"),
+        (struct.pack("<Q", 0), "Invalid TensorRT engine trailer"),
+        (
+            struct.pack("<QI", _ENGINE_MAGIC, 999),
+            "Unsupported TensorRT engine version",
+        ),
+        (
+            struct.pack("<QI", _ENGINE_MAGIC, 1) + _string(b""),
+            "embedded TensorRT engine is empty",
+        ),
+    ],
+)
+def test_reject_malformed_engine_trailer(trailer, message):
+    module = _external_modules(_codegen(_relu_partition()))[0]
+    with pytest.raises(tvm.error.InternalError, match=message):
+        _load_bytes(_legacy_bytes(module) + trailer)
+
+
+@requires_runtime
[email protected]("batch", ["dynamic", 0])
+def test_eager_requires_static_positive_dimensions(batch):
+    dimension = tirx.Var("batch", "int64") if batch == "dynamic" else batch
+    with pytest.raises(ValueError, match="static positive"):
+        _codegen(_relu_partition((dimension, 4)), eager=True)
+
+
+@requires_runtime
+def test_eager_rejects_int8_calibration(monkeypatch):
+    monkeypatch.setenv("TVM_TENSORRT_USE_INT8", "1")
+    monkeypatch.setenv("TENSORRT_NUM_CALI_INT8", "1")
+    with pytest.raises(tvm.error.InternalError, match="INT8 calibration"):
+        _codegen(_relu_partition(), eager=True)
+
+
+def _two_partitions(dtype):
+    data, first, second, third = [
+        relax.Var(name, relax.TensorType((2, 4), dtype))
+        for name in ("data", "first", "second", "third")
+    ]
+    builder = relax.BlockBuilder()
+    with builder.function("main", [data, first, second, third]):
+        with builder.dataflow():
+            difference = builder.emit(relax.op.subtract(second, data))
+            shifted = builder.emit(relax.op.add(difference, first))
+            separated = builder.emit(relax.op.sin(shifted))
+            result = builder.emit_output(relax.op.multiply(separated, third))
+        builder.emit_func_output(result)
+
+    parameters = {
+        "first": np.linspace(-0.3, 0.4, 8, dtype=dtype).reshape(2, 4),
+        "second": np.linspace(0.8, 2.2, 8, dtype=dtype).reshape(2, 4),
+        "third": np.linspace(1.2, 2.9, 8, dtype=dtype).reshape(2, 4),
+    }
+    patterns = [
+        ("tensorrt.subtract", is_op("relax.subtract")(wildcard(), wildcard())),
+        ("tensorrt.add", is_op("relax.add")(wildcard(), wildcard())),
+        ("tensorrt.multiply", is_op("relax.multiply")(wildcard(), wildcard())),
+    ]
+    partitioned = tvm.transform.Sequential(
+        [
+            relax.transform.BindParams("main", parameters),
+            # Leave sin on the TVM side to force two TensorRT partitions.
+            relax.transform.FuseOpsByPattern(patterns, bind_constants=True),
+            relax.transform.MergeCompositeFunctions(),
+        ]
+    )(builder.get())
+    return partitioned, parameters
+
+
+def _conv2d_relu_partition():
+    data = relax.Var("data", relax.TensorType((1, 2, 5, 5), "float32"))
+    weight = relax.Var("weight", relax.TensorType((3, 2, 3, 3), "float32"))
+    builder = relax.BlockBuilder()
+    with builder.function("main", [data, weight]):
+        with builder.dataflow():
+            convolution = builder.emit(relax.op.nn.conv2d(data, weight, 
padding=(1, 1)))
+            result = builder.emit_output(relax.op.nn.relu(convolution))
+        builder.emit_func_output(result)
+
+    weight_np = np.linspace(-0.3, 0.4, 54, dtype="float32").reshape(3, 2, 3, 3)
+    bound = relax.transform.BindParams("main", {"weight": 
weight_np})(builder.get())
+    partitioned = partition_for_tensorrt(bound)
+    data_np = np.linspace(-1.7, 1.3, 100, dtype="float32").reshape(2, 1, 2, 5, 
5)
+    padded = np.pad(data_np, ((0, 0), (0, 0), (0, 0), (1, 1), (1, 1)))
+    windows = np.lib.stride_tricks.sliding_window_view(padded, (3, 3), 
axis=(-2, -1))
+    expected = np.maximum(np.einsum("snihwkl,oikl->snohw", windows, 
weight_np), 0)
+    return partitioned, data_np, expected
+
+
+@requires_runtime
+@requires_gpu
[email protected]
+def test_failed_engine_build_can_be_retried():
+    """A failure after copying weights must not leave a partial engine or 
borrowed inputs."""
+    partitioned, _ = _two_partitions("float32")
+    offloaded = _codegen(partitioned, eager=False)
+    module = max(_external_modules(offloaded), key=lambda mod: 
len(mod["get_const_vars"]()))
+    graph = json.loads(module.inspect_source())
+    kernels = [node for node in graph["nodes"] if node["op"] == "kernel"]
+    kernels[-1]["name"] = "tensorrt.unsupported_test_op"
+    names = module["get_const_vars"]()
+    assert len(names) == 2
+    failing = tvm.get_global_func("runtime.tensorrt_runtime_create")(
+        module["get_symbol"](), json.dumps(graph), names
+    )
+    constants_by_name = dict(offloaded.attrs["const_name_to_constant"])
+    constants = [constants_by_name[name] for name in names]
+    legacy = _legacy_bytes(failing)
+
+    def check():
+        for _ in range(2):
+            with pytest.raises(tvm.error.InternalError, match="Unsupported 
operator"):
+                failing["build_engine"](constants)
+            assert _module_bytes(failing) == legacy
+            assert _module_bytes(_load_bytes(legacy)) == legacy
+
+    tvm.testing.run_with_gpu_lock(check)
+
+
+@requires_runtime
+@requires_gpu
[email protected](not env.has_llvm(), reason="LLVM is not available")
[email protected]
[email protected](sys.platform != "linux", reason="The builder guard uses 
LD_PRELOAD")
[email protected](
+    "dtype, device_id, model, execution_device",
+    [
+        ("float32", 0, "elementwise", "cuda"),
+        ("float16", 0, "elementwise", "cuda"),
+        ("float16", 1, "elementwise", "cuda"),
+        ("float32", 0, "conv2d_relu", "cuda"),
+        ("float32", 0, "elementwise", "cpu"),
+    ],
+)
+def test_exported_engines_run_without_builder(
+    tmp_path, monkeypatch, dtype, device_id, model, execution_device
+):
+    """Build weighted partitions before VM creation, then load and run without 
a builder."""
+    if not tvm.cuda(device_id).exist:
+        pytest.skip(f"CUDA device {device_id} is not available")
+
+    target = "llvm" if execution_device == "cpu" else "cuda"
+    guard_source = tmp_path / "builder_guard.cc"
+    guard_library = tmp_path / "builder_guard.so"
+    guard_source.write_text(
+        "#include <cstdlib>\n"
+        'extern "C" void* createInferBuilder_INTERNAL(void*, int) { 
std::_Exit(86); }\n'
+    )
+    cc.create_shared(str(guard_library), [str(guard_source)])
+
+    if model == "conv2d_relu":
+        partitioned, data, expected = _conv2d_relu_partition()
+        expected_partition_count = 1
+        # Match the numerical tolerance of the existing TensorRT convolution 
tests.
+        tolerance = 1e-3
+    else:
+        partitioned, parameters = _two_partitions(dtype)
+        data = np.linspace(-1.7, 1.3, 16, dtype=dtype).reshape(2, 2, 4)
+        expected = np.sin(parameters["second"] - data + parameters["first"]) * 
parameters["third"]
+        expected_partition_count = 2
+        tolerance = 1e-2 if dtype == "float16" else 1e-5
+    np.savez(tmp_path / "reference.npz", data=data, expected=expected)
+
+    runner = textwrap.dedent(
+        """
+        import sys
+        import numpy as np
+        import tvm
+        from tvm import relax
+
+        artifact = tvm.runtime.load_module(sys.argv[1])
+        device = tvm.cpu() if sys.argv[5] == "cpu" else 
tvm.cuda(int(sys.argv[3]))
+        tolerance = float(sys.argv[4])
+        vm = relax.VirtualMachine(artifact, device)
+        reference = np.load(sys.argv[2])
+        for data, expected in zip(reference["data"], reference["expected"]):
+            result = vm["main"](tvm.runtime.tensor(data, device))
+            assert result.device == device
+            np.testing.assert_allclose(result.numpy(), expected, 
rtol=tolerance, atol=tolerance)
+        """
+    )
+    child_env = os.environ.copy()
+    child_env.pop("TVM_TENSORRT_CACHE_DIR", None)
+    previous_preload = child_env.get("LD_PRELOAD", "")
+    child_env["LD_PRELOAD"] = str(guard_library) + (
+        ":" + previous_preload if previous_preload else ""
+    )
+
+    def run_child(path):
+        return subprocess.run(
+            [
+                sys.executable,
+                "-c",
+                runner,
+                str(path),
+                str(tmp_path / "reference.npz"),
+                str(device_id),
+                str(tolerance),
+                execution_device,
+            ],
+            env=child_env,
+            capture_output=True,
+            text=True,
+            timeout=120,
+            check=False,
+        )
+
+    def check():
+        # Negative control: prove the preload intercepts the existing lazy 
builder.
+        lazy = _codegen(partitioned, eager=False)
+        assert len(_external_modules(lazy)) == expected_partition_count
+        lazy_path = tmp_path / "lazy.so"
+        tvm.compile(lazy, target).export_library(str(lazy_path))
+        result = run_child(lazy_path)
+        assert result.returncode == 86, result.stdout + result.stderr
+
+        eager = _codegen(partitioned, eager=True)
+        modules = _external_modules(eager)
+        assert len(modules) == expected_partition_count
+        for module in modules:
+            payload = _module_bytes(module)
+            # No VirtualMachine has been created in this process.
+            assert len(payload) > len(_legacy_bytes(module))
+            restored = _load_bytes(payload)
+            assert _module_bytes(restored) == payload
+
+        eager_path = tmp_path / "eager.so"
+        tvm.compile(eager, target).export_library(str(eager_path))
+        result = run_child(eager_path)
+        assert result.returncode == 0, result.stdout + result.stderr
+
+        module = modules[0]
+        constants_by_name = dict(eager.attrs["const_name_to_constant"])
+        constants = [constants_by_name[name] for name in 
module["get_const_vars"]()]
+        # Embedded plans cannot accept either an INT8 flag or a captured 
calibration count.
+        for use_int8 in ("1", "0"):
+            with monkeypatch.context() as calibration:
+                calibration.setenv("TVM_TENSORRT_USE_INT8", use_int8)
+                calibration.setenv("TENSORRT_NUM_CALI_INT8", "1")
+                loaded = _load_bytes(_module_bytes(module))
+                with pytest.raises(
+                    tvm.error.InternalError, match="prebuilt TensorRT.*INT8 
calibration"
+                ):
+                    loaded["__init_" + loaded["get_symbol"]()](constants)
+
+        # A corrupt plan must fail deserialization instead of rebuilding from 
the JSON.
+        payload = bytearray(_module_bytes(module))
+        plan_start = len(_legacy_bytes(module)) + 8 + 4 + 8
+        payload[plan_start : plan_start + 32] = b"\0" * 32
+        corrupt = _load_bytes(bytes(payload))
+        symbol = corrupt["get_symbol"]()
+        corrupt["__init_" + symbol](constants)
+        # Engine deserialization is deferred until the input device is known.
+        device = tvm.cpu() if execution_device == "cpu" else 
tvm.cuda(device_id)
+        argument = tvm.runtime.tensor(np.zeros(data.shape[1:], dtype=dtype), 
device)
+        output = tvm.runtime.tensor(np.zeros(expected.shape[1:], dtype=dtype), 
device)
+        with pytest.raises(
+            tvm.error.InternalError, match="deserialize the embedded TensorRT 
engine"
+        ):
+            corrupt[symbol](argument, output)
+
+    tvm.testing.run_with_gpu_lock(check)
+
+
+if __name__ == "__main__":
+    tvm.testing.main()

Reply via email to