|
| 1 | +--- |
| 2 | +name: trt-cpp-runtime-quickstart |
| 3 | +description: >- |
| 4 | + Load and run a TensorRT engine (.plan / .engine) from C++ using the |
| 5 | + TensorRT 11 / 10.x **modern Runtime API**, avoiding the deprecated TRT |
| 6 | + 8.x binding-index APIs that older guidance still promotes. Use whenever |
| 7 | + the user asks about loading |
| 8 | + or running a TensorRT .plan/.engine from C++, even on "minimal example" |
| 9 | + requests — without this skill the default reply uses deprecated |
| 10 | + enqueueV2-style code. Also use when the user hits "Engine plan file |
| 11 | + is generated on an incompatible device", deserializeCudaEngine returns |
| 12 | + nullptr, gets an enqueueV2 / IStreamReader deprecation warning, or |
| 13 | + wants to stream a .plan via IStreamReaderV2. Triggers: TensorRT C++ |
| 14 | + inference, load TensorRT plan C++, run .plan from C++, IRuntime |
| 15 | + example, deserializeCudaEngine, enqueueV3, enqueueV2 deprecated, |
| 16 | + setTensorAddress, getBindingIndex, IStreamReaderV2, libnvinfer C++. |
| 17 | + NOT for building engines (`trt-onnx-quickstart`), Python deploy, |
| 18 | + plugins, multi-GPU. |
| 19 | +license: Apache-2.0 |
| 20 | +metadata: |
| 21 | + author: NVIDIA Corporation |
| 22 | + version: "1.0" |
| 23 | + tags: |
| 24 | + - tensorrt |
| 25 | + - cpp |
| 26 | + - inference |
| 27 | + - deployment |
| 28 | + - runtime |
| 29 | +--- |
| 30 | + |
| 31 | +# TensorRT C++ Runtime Deploy |
| 32 | + |
| 33 | +Load a serialized TensorRT engine from disk and run inference from C++ using only the modern Runtime API. Produces a minimal, copy-pasteable deploy harness that drops next to any `.plan` / `.engine` file and extends to production. |
| 34 | + |
| 35 | +Reference samples to open before writing new code: |
| 36 | + |
| 37 | +- `quickstart/SemanticSegmentation/tutorial-runtime.cpp` — cleanest minimal load-and-run example. Mirrors Steps 1–7 below. |
| 38 | +- `samples/sampleOnnxMNIST/sampleOnnxMNIST.cpp` — end-to-end sample that also builds the engine; the runtime portion shows realistic I/O wiring. |
| 39 | +- Public headers: `include/NvInferRuntime.h` — read `IRuntime`, `ICudaEngine`, `IExecutionContext`, `IStreamReaderV2`. |
| 40 | + |
| 41 | +## When to Use |
| 42 | + |
| 43 | +| Situation | Use this skill? | |
| 44 | +|-------------------------------------------------------------------------------------------|-----------------| |
| 45 | +| You have a `.plan`/`.engine` and need to run it from a C++ binary | Yes | |
| 46 | +| You need a minimal harness that uses `enqueueV3` + `setTensorAddress` | Yes | |
| 47 | +| You want to load an engine from a `std::istream` or large file via `IStreamReaderV2` | Yes | |
| 48 | +| You need to wire dynamic shapes (`setInputShape`) before inference | Yes | |
| 49 | +| You are *building* / optimizing the engine (calibration, INT8, sparsity, builder configs) | No - use trtexec or `IBuilder` directly | |
| 50 | +| You are deploying in Python | No - use `tensorrt` Python bindings | |
| 51 | +| You are writing a plugin (`IPluginV3`) or custom layer | No - separate plugin skill | |
| 52 | +| You need multi-GPU, MPS, MIG, or process-level orchestration | No - out of scope | |
| 53 | + |
| 54 | +## Prerequisites |
| 55 | + |
| 56 | +1. **TensorRT installed.** Verify `NvInferRuntime.h` is on the include path |
| 57 | + and `libnvinfer.so` is on the link path. On a TRT dev container these are |
| 58 | + in `/usr/include/x86_64-linux-gnu/` and `/usr/lib/x86_64-linux-gnu/` (or |
| 59 | + `/opt/tensorrt/...` for tarball installs). |
| 60 | +2. **CUDA toolkit available.** `cuda_runtime_api.h` and `libcudart.so` must |
| 61 | + be reachable; `nvcc --version` should match the CUDA version the engine |
| 62 | + was built against. |
| 63 | +3. **A serialized engine.** A `.plan`/`.engine` file built **on the same |
| 64 | + major TRT version and the same GPU architecture (compute capability) you |
| 65 | + will deploy on**. Engines are not portable across major TRT versions or |
| 66 | + across SMs unless the builder was given `--hardwareCompatibilityLevel`. |
| 67 | +4. **The engine's I/O tensor names.** Inspect with: |
| 68 | + ```bash |
| 69 | + trtexec --loadEngine=model.plan --verbose 2>&1 | grep -E 'Input|Output' |
| 70 | + ``` |
| 71 | +5. A C++17 compiler (`g++ >= 9` or `clang++ >= 10`). |
| 72 | + |
| 73 | +## Step 1: Create the IRuntime |
| 74 | + |
| 75 | +The runtime owns engine deserialization and must outlive every |
| 76 | +`ICudaEngine` it creates. Construct one per process for typical deployments. |
| 77 | + |
| 78 | +```cpp |
| 79 | +class Logger : public nvinfer1::ILogger { |
| 80 | +public: |
| 81 | + void log(Severity severity, char const* msg) noexcept override { |
| 82 | + if (severity <= Severity::kWARNING) { |
| 83 | + std::cerr << msg << std::endl; |
| 84 | + } |
| 85 | + } |
| 86 | +}; |
| 87 | + |
| 88 | +Logger gLogger; |
| 89 | +std::unique_ptr<nvinfer1::IRuntime> runtime{ |
| 90 | + nvinfer1::createInferRuntime(gLogger)}; |
| 91 | +if (!runtime) throw std::runtime_error("createInferRuntime failed"); |
| 92 | +``` |
| 93 | +
|
| 94 | +A custom logger is mandatory - TensorRT does not log internally. Keep it |
| 95 | +process-global so deserialization warnings (version skew, calibrator |
| 96 | +mismatch) are not lost. |
| 97 | +
|
| 98 | +## Step 2: Read the Plan into Memory |
| 99 | +
|
| 100 | +For small/medium engines (< ~1 GiB) read the whole file into a |
| 101 | +`std::vector<char>` and hand the pointer to |
| 102 | +`IRuntime::deserializeCudaEngine(blob, size)`. This is what the |
| 103 | +`SemanticSegmentation` tutorial does and the simplest correct path: |
| 104 | +
|
| 105 | +```cpp |
| 106 | +std::ifstream f(planPath, std::ios::binary); |
| 107 | +if (!f) throw std::runtime_error("cannot open " + planPath); |
| 108 | +f.seekg(0, std::ios::end); |
| 109 | +auto size = static_cast<size_t>(f.tellg()); |
| 110 | +f.seekg(0, std::ios::beg); |
| 111 | +std::vector<char> blob(size); |
| 112 | +if (!f.read(blob.data(), size)) |
| 113 | + throw std::runtime_error("short read on " + planPath); |
| 114 | +``` |
| 115 | + |
| 116 | +For very large engines, or when the bytes live behind a stream (HTTP, |
| 117 | +mmap'd archive, encrypted store), implement an `IStreamReaderV2` - see |
| 118 | +Step 3. |
| 119 | + |
| 120 | +## Step 3 (optional): Use IStreamReaderV2 for Streaming Loads |
| 121 | + |
| 122 | +`IStreamReader` (v1) is **deprecated in TensorRT 11.0**. Always use |
| 123 | +`IStreamReaderV2`: it reads into both host and device memory and is the |
| 124 | +only stream-reader form guaranteed for new code. Subclass and implement |
| 125 | +`read(...)` and `seek(...)`: |
| 126 | + |
| 127 | +```cpp |
| 128 | +class FileStreamReader : public nvinfer1::IStreamReaderV2 { |
| 129 | +public: |
| 130 | + explicit FileStreamReader(std::string const& path) |
| 131 | + : mFile(path, std::ios::binary) { |
| 132 | + if (!mFile) throw std::runtime_error("open failed: " + path); |
| 133 | + } |
| 134 | + int64_t read(void* dst, int64_t n, |
| 135 | + cudaStream_t /*stream*/) noexcept override { |
| 136 | + mFile.read(static_cast<char*>(dst), n); |
| 137 | + return mFile.gcount(); |
| 138 | + } |
| 139 | + bool seek(int64_t off, nvinfer1::SeekPosition where) noexcept override { |
| 140 | + auto dir = (where == nvinfer1::SeekPosition::kSET) ? std::ios::beg |
| 141 | + : (where == nvinfer1::SeekPosition::kCUR) ? std::ios::cur |
| 142 | + : std::ios::end; |
| 143 | + mFile.clear(); |
| 144 | + mFile.seekg(off, dir); |
| 145 | + return static_cast<bool>(mFile); |
| 146 | + } |
| 147 | +private: |
| 148 | + std::ifstream mFile; |
| 149 | +}; |
| 150 | + |
| 151 | +FileStreamReader rd{planPath}; |
| 152 | +std::unique_ptr<nvinfer1::ICudaEngine> engine{ |
| 153 | + runtime->deserializeCudaEngine(rd)}; |
| 154 | +``` |
| 155 | +
|
| 156 | +## Step 4: Deserialize and Create an Execution Context |
| 157 | +
|
| 158 | +`ICudaEngine` is thread-safe for read-only queries; `IExecutionContext` |
| 159 | +is **not** - allocate one per inference thread. |
| 160 | +
|
| 161 | +```cpp |
| 162 | +std::unique_ptr<nvinfer1::ICudaEngine> engine{ |
| 163 | + runtime->deserializeCudaEngine(blob.data(), blob.size())}; |
| 164 | +if (!engine) throw std::runtime_error("deserializeCudaEngine failed"); |
| 165 | +
|
| 166 | +std::unique_ptr<nvinfer1::IExecutionContext> ctx{ |
| 167 | + engine->createExecutionContext()}; |
| 168 | +if (!ctx) throw std::runtime_error("createExecutionContext failed"); |
| 169 | +``` |
| 170 | + |
| 171 | +## Step 5: Wire Tensors with setTensorAddress |
| 172 | + |
| 173 | +Enumerate I/O tensors via `getNbIOTensors()` + `getIOTensorName(i)`. Use |
| 174 | +`getTensorIOMode`, `getTensorDataType`, and `getTensorShape` to size and |
| 175 | +allocate buffers. **Set every tensor address before `enqueueV3`** - the |
| 176 | +modern API has no implicit binding-index map. |
| 177 | + |
| 178 | +```cpp |
| 179 | +for (int i = 0; i < engine->getNbIOTensors(); ++i) { |
| 180 | + char const* name = engine->getIOTensorName(i); |
| 181 | + auto mode = engine->getTensorIOMode(name); |
| 182 | + auto shape = engine->getTensorShape(name); // -1 = dynamic dim |
| 183 | + if (mode == nvinfer1::TensorIOMode::kINPUT && hasDynamic(shape)) { |
| 184 | + // Fill in concrete shape, e.g. batch=1 |
| 185 | + shape.d[0] = 1; |
| 186 | + ctx->setInputShape(name, shape); |
| 187 | + } |
| 188 | +} |
| 189 | +// After setInputShape on all dynamic inputs, query output shapes. |
| 190 | +for (int i = 0; i < engine->getNbIOTensors(); ++i) { |
| 191 | + char const* name = engine->getIOTensorName(i); |
| 192 | + auto bytes = elementCount(ctx->getTensorShape(name)) |
| 193 | + * dtypeSize(engine->getTensorDataType(name)); |
| 194 | + void* dev = nullptr; |
| 195 | + cudaMalloc(&dev, bytes); |
| 196 | + ctx->setTensorAddress(name, dev); |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +Always call `setInputShape` for dynamic inputs **before** querying output |
| 201 | +shapes - the latter depends on the former. |
| 202 | + |
| 203 | +## Step 6: Run enqueueV3 |
| 204 | + |
| 205 | +`enqueueV3(stream)` is the only non-deprecated enqueue API; |
| 206 | +`enqueueV2`/`execute*` are gone in modern flows. |
| 207 | + |
| 208 | +```cpp |
| 209 | +cudaStream_t stream{}; |
| 210 | +cudaStreamCreate(&stream); |
| 211 | + |
| 212 | +cudaMemcpyAsync(devInput, hostInput, inBytes, |
| 213 | + cudaMemcpyHostToDevice, stream); |
| 214 | +if (!ctx->enqueueV3(stream)) |
| 215 | + throw std::runtime_error("enqueueV3 failed"); |
| 216 | +cudaMemcpyAsync(hostOutput, devOutput, outBytes, |
| 217 | + cudaMemcpyDeviceToHost, stream); |
| 218 | +cudaStreamSynchronize(stream); |
| 219 | +``` |
| 220 | +
|
| 221 | +If you reuse buffers across iterations, skip the per-call |
| 222 | +`setTensorAddress` - addresses persist on the context until overwritten. |
| 223 | +
|
| 224 | +## Step 7: Shutdown Order |
| 225 | +
|
| 226 | +Destroy in reverse construction order: contexts -> engines -> runtime, |
| 227 | +then free CUDA memory and destroy the stream. With `std::unique_ptr` this |
| 228 | +is automatic as long as the context is declared *after* the engine, and |
| 229 | +the engine *after* the runtime. Free `cudaMalloc` allocations explicitly |
| 230 | +(RAII wrapper recommended). |
| 231 | +
|
| 232 | +## Build |
| 233 | +
|
| 234 | +Wire the steps above into your application's build system. For a standalone smoke test, a minimal build is: |
| 235 | +
|
| 236 | +```bash |
| 237 | +g++ -std=c++17 runtime.cpp -o run -lnvinfer -lcudart # adjust CUDA/TRT include + lib paths |
| 238 | +./run model.plan |
| 239 | +``` |
| 240 | + |
| 241 | +## Common Errors |
| 242 | + |
| 243 | +| Symptom | Likely cause | |
| 244 | +|----------------------------------------------------------------------|------------------------------------------------------------------------------| |
| 245 | +| `deserializeCudaEngine` returns `nullptr`, log says "version tag" | Engine built on a different TRT major version. Rebuild on the deploy version | |
| 246 | +| `nullptr` with "engine plan file is generated on an incompatible device" | SM mismatch. Rebuild on the target SM or use `--hardwareCompatibilityLevel` | |
| 247 | +| `enqueueV3` returns false, log mentions "Tensor X has no address" | Forgot `setTensorAddress` for one of the I/O tensors | |
| 248 | +| `enqueueV3` false, "shape" in message | Forgot `setInputShape` for a dynamic input, or supplied an out-of-profile shape | |
| 249 | +| `cudaErrorIllegalAddress` on H->D / D->H copy | Mismatched element count / dtype between host buffer and engine tensor | |
| 250 | +| Process crashes inside TRT during destruction | Wrong destruction order - context outlived engine, or engine outlived runtime | |
| 251 | +| `cudaErrorMemoryAllocation` during context creation | Workspace too big for the device; rebuild with smaller workspace | |
| 252 | + |
| 253 | +## Pitfalls |
| 254 | + |
| 255 | +- **Do not use `IStreamReader` v1.** Deprecated in TRT 11.0. Use |
| 256 | + `IStreamReaderV2` (note `cudaStream_t` parameter on `read`). |
| 257 | +- **Do not use `enqueueV2` / `execute` / binding indices.** These are |
| 258 | + legacy paths; the only stable modern path is name-based |
| 259 | + `setTensorAddress` + `enqueueV3`. |
| 260 | +- **One `IExecutionContext` per thread.** Sharing contexts across threads |
| 261 | + is undefined behavior; sharing the engine is fine. |
| 262 | +- **Stream lifetime.** The CUDA stream passed to `enqueueV3` must outlive |
| 263 | + the inference. Destroying it while work is in flight crashes or corrupts |
| 264 | + output. |
| 265 | +- **Async vs sync copies.** Mixing synchronous `cudaMemcpy` with |
| 266 | + `enqueueV3` on a stream serializes the GPU; always pair `enqueueV3` |
| 267 | + with `cudaMemcpyAsync` on the same stream. |
| 268 | +- **Engine portability.** A `.plan` is tied to (TRT major version, GPU SM, |
| 269 | + CUDA major version). Never check engines into a repo without recording |
| 270 | + these three facts. |
| 271 | +- **Logger lifetime.** The logger passed to `createInferRuntime` must |
| 272 | + outlive the runtime; a stack-local logger in `main` is fine, a function- |
| 273 | + scope local is a use-after-free. |
| 274 | +- **Refit / weight streaming.** Engines built with refit or weight |
| 275 | + streaming enabled need extra setup calls (`setWeightStreamingBudgetV2`, |
| 276 | + `IRefitter`); out of scope here. |
0 commit comments