This is an automated email from the ASF dual-hosted git repository.
tvalentyn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new bdf650a75b5 Support TensorRT 10 and later in
TensorRTEngineHandlerNumPy (#39922)
bdf650a75b5 is described below
commit bdf650a75b54ab76c01025622fb31f91420359d2
Author: akshayjadiyanv <[email protected]>
AuthorDate: Tue Sep 15 13:55:51 2026 -0700
Support TensorRT 10 and later in TensorRTEngineHandlerNumPy (#39922)
* Fix stale metrics namespace assertion in TensorRT test
test_namespace has asserted 'RunInferenceTensorRT' since the original
TensorRT commit (a8ca3057c0b). The handler was later changed to return
'BeamML_TensorRT' in f477b85f230, matching the BeamML_* prefix that every
other model handler uses, but the test was never updated.
The mismatch went unnoticed because the TensorRT suite does not run in any
active CI job.
* Support TensorRT 10 and later in TensorRTEngineHandlerNumPy
TensorRT 10 removed the index based binding API that the handler was written
against, so RunInference fails at engine load time with:
AttributeError: 'ICudaEngine' object has no attribute 'num_bindings'
Select the API at runtime from the TensorRT major version rather than
picking
one of them. TensorRT 8.x keeps the binding API and execute_async_v2, while
TensorRT 10 and later use the name based tensor API and execute_async_v3. No
currently supported GPU loses support.
Supporting both versions is necessary rather than merely convenient.
Dataflow
now offers Blackwell GPUs (RTX Pro 6000, compute capability 12.0) that no
TensorRT 8.x release can target, while TensorRT 10 and later require compute
capability 7.5 or higher and so cannot target the Pascal and Volta GPUs that
Dataflow still offers. No single TensorRT version covers the whole range.
Also handle cuda-python 13, which removed the cuda.cuda alias in favour of
cuda.bindings.driver, and move the test container to
nvcr.io/nvidia/tensorrt:26.06-py3 (TensorRT 11.0, CUDA 13.3, Python 3.12).
Because that image is Python 3.12, the disabled tensorRTtests task moves
from
the py310 suite to the py312 suite.
The Dataflow integration test stays disabled. Every .trt engine staged under
gs://apache-beam-ml/models/ was built with TensorRT 8.x, and a serialized
engine can only be read by the major version that built it. Rebuilt and
verified replacements are available, but staging them needs write access to
that bucket; see the pull request description.
Verified on a T4 GPU on GCE: 7/7 tests pass under TensorRT 11.0
(nvcr.io/nvidia/tensorrt:26.06-py3) and 7/7 under TensorRT 8.6.1
(23.05-py3).
Addresses #36306
Addresses #33946
* Pass host buffers to CUDA by address in TensorRT inference
_default_tensorRT_inference_fn passed numpy arrays directly to
cuMemcpyHtoDAsync and cuMemcpyDtoHAsync. An array holding exactly one
element is coerced to a scalar rather than being handled through the buffer
protocol, so the value is read as a null host pointer and the copy fails:
RuntimeError: Cuda Error: <CUresult.CUDA_ERROR_INVALID_VALUE: 1>
Pass the buffer address explicitly instead. This is not specific to a
TensorRT or cuda-python version; it reproduces on cuda-python 12.9 and 13.3
alike, and depends only on an input or output tensor having a single
element.
Single element tensors are common. The ssd_mobilenet_v2_320x320_coco17_tpu-8
model behind the tensorRTtests integration test has a num_detections output
of shape (1, 1), so that test cannot pass without this fix. The existing
unit
tests did not catch it because every tensor in their models holds four
elements.
The two copies of this loop in the test file are updated to match, so they
do
not keep demonstrating the broken pattern.
* Add a script to rebuild the staged TensorRT test engines
A serialized TensorRT engine can only be deserialized by the same TensorRT
major version and GPU architecture that built it, so the engines the tests
load from gs://apache-beam-ml/models/ have to be rebuilt whenever the
TensorRT version in tensor_rt.dockerfile changes, or the tensorRTtests task
moves to a different GPU. Until now that was undocumented manual work, which
is part of why the staged engines went stale.
build_test_engines.py rebuilds each of the three from the ONNX source
already
staged beside it, so no new model sources are needed, and verifies the
result
by loading it back through TensorRTEngineHandlerNumPy: the two small engines
against the exact values the unit tests assert, and the object detection
engine against the same COCO images the integration test uses. Engines are
only uploaded once verification passes.
The script needs a GPU, so it cannot run as part of the test suite.
README.md
covers how to run it and when it needs running.
* Address review feedback on TensorRT version support
- Cache _trt_major_version() and _import_cuda_driver() with
functools.lru_cache. _import_cuda_driver() is called from
_assign_or_fail(),
so it ran on every CUDA call. Caching rather than resolving at import time
keeps the module importable without TensorRT, so jobs can still be
submitted
from a machine that does not have it.
- Pin torch in the TensorRT documentation rather than leaving it unpinned.
- Trim the two CHANGES.md entries to the change itself.
- Drop the TensorRT 8 branch from build_test_engines.py. The script only
rebuilds engines for the container the tests currently use, so it can
require TensorRT 10 or later. The equivalent branch in the handler stays,
since the handler does support both.
* Require TensorRT 10 or later instead of supporting both APIs
Per review discussion, bump the lower bound rather than carrying two code
paths. TensorRT 8.x is from 2023 and keeping both APIs alive makes the
handler
harder to maintain for a shrinking set of users.
Removes _trt_major_version() and _network_creation_flags() and the branches
they fed, so engine setup always uses the name based tensor API and
execution
always uses execute_async_v3. The numpy monkey patch that only existed for
TensorRT 8.x goes with them.
_check_trt_version() replaces them. It raises a clear error naming the
installed version, rather than letting an unsupported TensorRT surface as an
AttributeError deep inside engine setup. It stays lazy and cached so the
module remains importable without TensorRT, which is what lets jobs be
submitted from a machine that does not have it.
CHANGES.md records this as a breaking change and asks anyone hard blocked by
it to comment on #36306.
* Check the TensorRT version before deserializing an engine
_load_engine() deserialized first and only reached _check_trt_version() when
TensorRTEngine was constructed, so loading a pre-built engine on an
unsupported TensorRT failed with an opaque deserialization error instead of
the message naming the installed version. Loading a pre-built engine is the
most common path, and the one users hitting the new lower bound will take.
* Update sdks/python/apache_beam/examples/inference/README.md
* Update sdks/python/apache_beam/examples/inference/README.md
---------
Co-authored-by: tvalentyn <[email protected]>
---
CHANGES.md | 5 +
.../apache_beam/examples/inference/README.md | 10 +-
.../apache_beam/ml/inference/tensorrt_inference.py | 100 +++++++--
.../ml/inference/tensorrt_inference_test.py | 67 +++---
.../containers/tensorrt_runinference/README.md | 73 +++++-
.../tensorrt_runinference/build_test_engines.py | 250 +++++++++++++++++++++
.../tensorrt_runinference/tensor_rt.dockerfile | 15 +-
sdks/python/test-suites/dataflow/common.gradle | 11 +-
.../en/documentation/ml/tensorrt-runinference.md | 8 +-
9 files changed, 470 insertions(+), 69 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index fc7d62ece33..18351eebd1a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -85,6 +85,10 @@
* Portable Java SDK now encodes SchemaCoders in a portable way
([#34672](https://github.com/apache/beam/issues/34672)).
- Original custom Java coder encoding can still be obtained using
[StreamingOptions.setUpdateCompatibilityVersion("2.76")](https://github.com/apache/beam/blob/2cf0930e7ae1aa389c26ce6639b584877a3e31d9/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java#L47)
([#34672](https://github.com/apache/beam/issues/34672)).
- Fixes ([#36496](https://github.com/apache/beam/issues/36496)),
([#30276](https://github.com/apache/beam/issues/30276)),
([#29245](https://github.com/apache/beam/issues/29245)).
+* (Python) `TensorRTEngineHandlerNumPy` now requires TensorRT 10 or later.
TensorRT 8.x is no longer supported, since TensorRT 10 removed the engine
binding API the handler was written against
([#36306](https://github.com/apache/beam/issues/36306)).
+ - Engines serialized by TensorRT 8.x must be rebuilt, as an engine can only
be deserialized by the major version that built it.
+ - TensorRT 10 and later require a GPU with compute capability 7.5 or higher,
which excludes NVIDIA Pascal and Volta GPUs.
+ - If dropping TensorRT 8.x support is a hard blocker for you, please comment
on ([#36306](https://github.com/apache/beam/issues/36306)).
## Deprecations
@@ -99,6 +103,7 @@
* (Prism) Self-checkpointing splittable DoFns now resume after their requested
delay instead of immediately, so polling SDFs no longer busy-spin
([#39848](https://github.com/apache/beam/issues/39848)).
* (Java) MongoDbIO read splitting now preserves non-ObjectId `_id` types (e.g.
string ids) instead of failing to parse the generated range filters
([#39900](https://github.com/apache/beam/issues/39900)).
* (Go) Fixed GCS glob matching silently dropping objects when the glob pattern
contains multi-byte characters
([#39969](https://github.com/apache/beam/issues/39969)).
+* (Python) Fixed `TensorRTEngineHandlerNumPy` failing with
`CUDA_ERROR_INVALID_VALUE` on models with a single-element input or output
tensor ([#36306](https://github.com/apache/beam/issues/36306)).
## Security Fixes
diff --git a/sdks/python/apache_beam/examples/inference/README.md
b/sdks/python/apache_beam/examples/inference/README.md
index 5eed659d068..c43ad458347 100644
--- a/sdks/python/apache_beam/examples/inference/README.md
+++ b/sdks/python/apache_beam/examples/inference/README.md
@@ -83,13 +83,19 @@ pip install torch==1.10.0
### TensorRT dependencies
The RunInference API supports TensorRT SDK for high-performance deep learning
inference with NVIDIA GPUs.
-To use TensorRT locally, we suggest an environment with TensorRT >= 8.0.1.
Install TensorRT as per the
+To use TensorRT locally, we suggest an environment with TensorRT >= 10.0.
Install TensorRT as per the
[TensorRT Install
Guide](https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html).
You
will need to make sure the Python bindings for TensorRT are also installed
correctly, these are available by installing the python3-libnvinfer and
python3-libnvinfer-dev packages on your TensorRT download.
+TensorRT 10 or later is required. Note that a serialized TensorRT engine can
only
+be deserialized by the TensorRT major version that built it, so an engine built
+with TensorRT 8.x must be rebuilt. TensorRT 10 and later also require a GPU
with
+compute capability 7.5 or higher, for example, T4, L4, A100. The NVIDIA Pascal
and Volta GPUs
+such as the Tesla P4, P100 and V100 are no longer supported.
+
If you would like to use Docker, you can use an NGC image like:
```
-docker pull nvcr.io/nvidia/tensorrt:22.04-py3
+docker pull nvcr.io/nvidia/tensorrt:26.06-py3
```
as an existing container base to [build custom Apache Beam
container](https://beam.apache.org/documentation/runtime/environments/#modify-existing-base-image).
diff --git a/sdks/python/apache_beam/ml/inference/tensorrt_inference.py
b/sdks/python/apache_beam/ml/inference/tensorrt_inference.py
index 333187301b2..b6089788eae 100644
--- a/sdks/python/apache_beam/ml/inference/tensorrt_inference.py
+++ b/sdks/python/apache_beam/ml/inference/tensorrt_inference.py
@@ -19,6 +19,7 @@
from __future__ import annotations
+import functools
import logging
import threading
from collections.abc import Callable
@@ -48,9 +49,58 @@ except ModuleNotFoundError:
'runner has tensorrt dependencies installed.'
LOGGER.warning(msg)
+MIN_TRT_MAJOR_VERSION = 10
+
+
[email protected]_cache(maxsize=1)
+def _check_trt_version() -> None:
+ """Fails fast if the installed TensorRT is older than we support.
+
+ TensorRT 10 removed the index based "binding" API this module used to be
+ written against. Without this check the failure surfaces as an obscure
+ AttributeError deep inside engine setup.
+
+ Cached rather than checked at import time because the module is importable
+ without TensorRT, so that jobs can be submitted from a machine that does not
+ have it installed.
+ """
+ import tensorrt as trt
+ try:
+ major = int(trt.__version__.split('.')[0])
+ except (AttributeError, IndexError, ValueError):
+ # Fall back to probing for an attribute that only exists from 10 onwards.
+ major = 10 if hasattr(trt.ICudaEngine, 'num_io_tensors') else 8
+ if major < MIN_TRT_MAJOR_VERSION:
+ raise RuntimeError(
+ 'RunInference requires TensorRT %d or later, but found %s. Support '
+ 'for TensorRT 8.x was removed because TensorRT 10 replaced the '
+ 'engine binding API this handler depends on.' %
+ (MIN_TRT_MAJOR_VERSION, getattr(trt, '__version__', 'unknown')))
+
+
[email protected]_cache(maxsize=1)
+def _import_cuda_driver():
+ """Imports the CUDA driver bindings.
+
+ ``cuda.bindings.driver`` is the module path used by cuda-python 12.8 and
+ later. It replaced the ``cuda.cuda`` alias, which was removed in
+ cuda-python 13.0, so only fall back to that for older installations.
+
+ Cached because this is called from _assign_or_fail, which runs on every
+ CUDA call.
+ """
+ try:
+ from cuda.bindings import driver as cuda
+ except ImportError:
+ from cuda import cuda
+ return cuda
+
def _load_engine(engine_path):
import tensorrt as trt
+ # Checked before deserializing, because an engine built by a newer TensorRT
+ # fails to deserialize with an opaque error that hides the real cause.
+ _check_trt_version()
file = FileSystems.open(engine_path, 'rb')
runtime = trt.Runtime(TRT_LOGGER)
engine = runtime.deserialize_cuda_engine(file.read())
@@ -60,9 +110,11 @@ def _load_engine(engine_path):
def _load_onnx(onnx_path):
import tensorrt as trt
+ _check_trt_version()
builder = trt.Builder(TRT_LOGGER)
- network = builder.create_network(
- flags=1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
+ # Explicit batch is the only supported mode from TensorRT 10 onwards, so no
+ # network creation flags are needed.
+ network = builder.create_network()
parser = trt.OnnxParser(network, TRT_LOGGER)
with FileSystems.open(onnx_path) as f:
if not parser.parse(f.read()):
@@ -85,7 +137,7 @@ def _build_engine(network, builder):
def _assign_or_fail(args):
"""CUDA error checking."""
- from cuda import cuda
+ cuda = _import_cuda_driver()
err, ret = args[0], args[1:]
if isinstance(err, cuda.CUresult):
if err != cuda.CUresult.CUDA_SUCCESS:
@@ -111,7 +163,8 @@ class TensorRTEngine:
engine: trt.ICudaEngine object that contains TensorRT engine
"""
import tensorrt as trt
- from cuda import cuda
+ _check_trt_version()
+ cuda = _import_cuda_driver()
self.engine = engine
self.context = engine.create_execution_context()
self.context_lock = threading.RLock()
@@ -120,19 +173,12 @@ class TensorRTEngine:
self.gpu_allocations = []
self.cpu_allocations = []
- # TODO(https://github.com/NVIDIA/TensorRT/issues/2557):
- # Clean up when fixed upstream.
- try:
- _ = np.bool
- except AttributeError:
- # numpy >= 1.24.0
- np.bool = np.bool_ # type: ignore
-
- # Setup I/O bindings.
- for i in range(self.engine.num_bindings):
- name = self.engine.get_binding_name(i)
- dtype = self.engine.get_binding_dtype(i)
- shape = self.engine.get_binding_shape(i)
+ # Setup I/O tensors. Device addresses are bound to the context once here
+ # because execute_async_v3 takes no allocation list at execution time.
+ for i in range(self.engine.num_io_tensors):
+ name = self.engine.get_tensor_name(i)
+ dtype = self.engine.get_tensor_dtype(name)
+ shape = self.engine.get_tensor_shape(name)
size = trt.volume(shape) * dtype.itemsize
allocation = _assign_or_fail(cuda.cuMemAlloc(size))
binding = {
@@ -144,7 +190,8 @@ class TensorRTEngine:
'size': size
}
self.gpu_allocations.append(allocation)
- if self.engine.binding_is_input(i):
+ self.context.set_tensor_address(name, int(allocation))
+ if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
self.inputs.append(binding)
else:
self.outputs.append(binding)
@@ -182,7 +229,7 @@ def _default_tensorRT_inference_fn(
engine: TensorRTEngine,
inference_args: Optional[dict[str,
Any]] = None) -> Iterable[PredictionResult]:
- from cuda import cuda
+ cuda = _import_cuda_driver()
(
engine,
context,
@@ -195,17 +242,26 @@ def _default_tensorRT_inference_fn(
# Process I/O and execute the network
with context_lock:
+ # Host buffers are passed as explicit addresses rather than as arrays.
+ # A numpy array holding exactly one element is coerced to a scalar, which
+ # is then read as a null host pointer and fails with CUDA_ERROR_INVALID_
+ # VALUE. Single element outputs are common, for example the num_detections
+ # output of an object detection model.
+ # host_input must stay referenced until the stream is synchronized below,
+ # because the copy is asynchronous.
+ host_input = np.ascontiguousarray(batch)
_assign_or_fail(
cuda.cuMemcpyHtoDAsync(
inputs[0]['allocation'],
- np.ascontiguousarray(batch),
+ host_input.ctypes.data,
inputs[0]['size'],
stream))
- context.execute_async_v2(gpu_allocations, stream)
+ # Tensor addresses were bound when the engine was created.
+ context.execute_async_v3(stream)
for output in range(len(cpu_allocations)):
_assign_or_fail(
cuda.cuMemcpyDtoHAsync(
- cpu_allocations[output],
+ cpu_allocations[output].ctypes.data,
outputs[output]['allocation'],
outputs[output]['size'],
stream))
diff --git a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
index 80a01b8f4d4..48931eee399 100644
--- a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
+++ b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
@@ -19,6 +19,7 @@
import os
import unittest
+from unittest import mock
import numpy as np
import pytest
@@ -37,6 +38,9 @@ try:
from apache_beam.ml.inference.base import PredictionResult
from apache_beam.ml.inference.base import RunInference
from apache_beam.ml.inference.tensorrt_inference import
TensorRTEngineHandlerNumPy
+ from apache_beam.ml.inference.tensorrt_inference import _assign_or_fail
+ from apache_beam.ml.inference.tensorrt_inference import _check_trt_version
+ from apache_beam.ml.inference.tensorrt_inference import _import_cuda_driver
except ImportError:
raise unittest.SkipTest('TensorRT dependencies are not installed')
@@ -90,23 +94,8 @@ def _compare_prediction_result(a, b):
for actual, expected in zip(a.inference, b.inference)))
-def _assign_or_fail(args):
- """CUDA error checking."""
- from cuda import cuda
- err, ret = args[0], args[1:]
- if isinstance(err, cuda.CUresult):
- if err != cuda.CUresult.CUDA_SUCCESS:
- raise RuntimeError("Cuda Error: {}".format(err))
- else:
- raise RuntimeError("Unknown error type: {}".format(err))
- # Special case so that no unpacking is needed at call-site.
- if len(ret) == 1:
- return ret[0]
- return ret
-
-
def _custom_tensorRT_inference_fn(batch, engine, inference_args):
- from cuda import cuda
+ cuda = _import_cuda_driver()
(
engine,
context,
@@ -119,17 +108,18 @@ def _custom_tensorRT_inference_fn(batch, engine,
inference_args):
# Process I/O and execute the network
with context_lock:
+ host_input = np.ascontiguousarray(batch)
_assign_or_fail(
cuda.cuMemcpyHtoDAsync(
inputs[0]['allocation'],
- np.ascontiguousarray(batch),
+ host_input.ctypes.data,
inputs[0]['size'],
stream))
- context.execute_async_v2(gpu_allocations, stream)
+ context.execute_async_v3(stream)
for output in range(len(cpu_allocations)):
_assign_or_fail(
cuda.cuMemcpyDtoHAsync(
- cpu_allocations[output],
+ cpu_allocations[output].ctypes.data,
outputs[output]['allocation'],
outputs[output]['size'],
stream))
@@ -189,8 +179,7 @@ class TensorRTRunInferenceTest(unittest.TestCase):
inference_runner = TensorRTEngineHandlerNumPy(
min_batch_size=4, max_batch_size=4)
builder = trt.Builder(LOGGER)
- network = builder.create_network(
- flags=1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
+ network = builder.create_network()
input_tensor = network.add_input(
name="input", dtype=trt.float32, shape=(4, 1))
weight_const = network.add_constant(
@@ -227,8 +216,7 @@ class TensorRTRunInferenceTest(unittest.TestCase):
max_batch_size=4,
inference_fn=_custom_tensorRT_inference_fn)
builder = trt.Builder(LOGGER)
- network = builder.create_network(
- flags=1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
+ network = builder.create_network()
input_tensor = network.add_input(
name="input", dtype=trt.float32, shape=(4, 1))
weight_const = network.add_constant(
@@ -263,8 +251,7 @@ class TensorRTRunInferenceTest(unittest.TestCase):
inference_runner = TensorRTEngineHandlerNumPy(
min_batch_size=4, max_batch_size=4)
builder = trt.Builder(LOGGER)
- network = builder.create_network(
- flags=1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
+ network = builder.create_network()
input_tensor = network.add_input(
name="input", dtype=trt.float32, shape=(4, 2))
weight_const = network.add_constant(
@@ -349,7 +336,26 @@ class TensorRTRunInferenceTest(unittest.TestCase):
inference_runner = TensorRTEngineHandlerNumPy(
min_batch_size=4, max_batch_size=4)
self.assertEqual(
- 'RunInferenceTensorRT', inference_runner.get_metrics_namespace())
+ 'BeamML_TensorRT', inference_runner.get_metrics_namespace())
+
+ def test_supported_tensorrt_exposes_expected_api(self):
+ """The installed TensorRT must expose the API this module is written to.
+
+ TensorRT 10 removed the index based binding API in favour of the name
+ based tensor API. _check_trt_version() rejects anything older, so a passing
+ version check and a missing API would mean the two have drifted apart.
+ """
+ _check_trt_version()
+ self.assertTrue(hasattr(trt.ICudaEngine, 'num_io_tensors'))
+ self.assertTrue(hasattr(trt.IExecutionContext, 'execute_async_v3'))
+
+ def test_version_check_rejects_unsupported_tensorrt(self):
+ """An unsupported TensorRT must fail with a clear message."""
+ with mock.patch.object(trt, '__version__', '8.6.1'):
+ _check_trt_version.cache_clear()
+ with self.assertRaisesRegex(RuntimeError, 'requires TensorRT 10'):
+ _check_trt_version()
+ _check_trt_version.cache_clear()
@pytest.mark.uses_tensorrt
@@ -381,7 +387,7 @@ class TensorRTRunInferencePipelineTest(unittest.TestCase):
raise Exception(
f'Loaded engine of type {type(engine)}, was ' +
'expecting multi_process_shared engine')
- from cuda import cuda
+ cuda = _import_cuda_driver()
(
engine,
context,
@@ -394,17 +400,18 @@ class TensorRTRunInferencePipelineTest(unittest.TestCase):
# Process I/O and execute the network
with context_lock:
+ host_input = np.ascontiguousarray(batch)
_assign_or_fail(
cuda.cuMemcpyHtoDAsync(
inputs[0]['allocation'],
- np.ascontiguousarray(batch),
+ host_input.ctypes.data,
inputs[0]['size'],
stream))
- context.execute_async_v2(gpu_allocations, stream)
+ context.execute_async_v3(stream)
for output in range(len(cpu_allocations)):
_assign_or_fail(
cuda.cuMemcpyDtoHAsync(
- cpu_allocations[output],
+ cpu_allocations[output].ctypes.data,
outputs[output]['allocation'],
outputs[output]['size'],
stream))
diff --git a/sdks/python/test-suites/containers/tensorrt_runinference/README.md
b/sdks/python/test-suites/containers/tensorrt_runinference/README.md
index 99fbf83cbd7..35d805c54d9 100644
--- a/sdks/python/test-suites/containers/tensorrt_runinference/README.md
+++ b/sdks/python/test-suites/containers/tensorrt_runinference/README.md
@@ -17,8 +17,77 @@
under the License.
-->
-# TensorRT Dockerfile for Beam
+# TensorRT test resources for Beam
-This directory contains the Dockerfiles required to run Beam pipelines that
use TensorRT.
+This directory contains the Dockerfile required to run Beam pipelines that use
TensorRT,
+and the script that rebuilds the TensorRT engines those tests load from GCS.
+
+## Container image
To build the image, run `docker build -f tensor_rt.dockerfile -t
us.gcr.io/apache-beam-testing/python-postcommit-it/tensor_rt:latest .`
+
+## Rebuilding the test engines
+
+The TensorRT tests load pre-built engines from `gs://apache-beam-ml/models/`:
+
+| Engine | Used by |
+| --- | --- |
+| `single_tensor_features_engine.trt` | `tensorrt_inference_test.py` |
+| `multiple_tensor_features_engine.trt` | `tensorrt_inference_test.py` |
+| `ssd_mobilenet_v2_320x320_coco17_tpu-8.trt` | the `tensorRTtests` Dataflow
integration test |
+
+**A serialized TensorRT engine is not portable.** It can only be deserialized
by the
+same TensorRT major version and the same GPU architecture that built it. So
these files
+must be rebuilt whenever either of the following changes:
+
+* the TensorRT version in `tensor_rt.dockerfile`, or
+* the GPU that the `tensorRTtests` task requests in
+ `sdks/python/test-suites/dataflow/common.gradle`.
+
+`build_test_engines.py` does that. It rebuilds each engine from the ONNX
source already
+staged next to it in the same bucket, so no new model sources are needed, and
it verifies
+each result by loading it back through `TensorRTEngineHandlerNumPy` — the
small engines
+against the exact values the unit tests assert, and the object detection
engine against
+the same COCO images the integration test uses.
+
+It needs a GPU, so it cannot run as part of the test suite. Run it in the same
container
+and on the same GPU type the tests use. As of writing that is
+`nvcr.io/nvidia/tensorrt:26.06-py3` on an `nvidia-tesla-t4`. It requires
TensorRT 10 or
+later, since the TensorRT 8 engines are the ones being replaced.
+
+The host needs a driver new enough for that container (580 or later), plus
Docker and
+the NVIDIA container toolkit. On a GCE deep learning VM image the toolkit is
already
+present but Docker may not be:
+
+```
+sudo apt-get install -y docker.io
+sudo nvidia-ctk runtime configure --runtime=docker && sudo systemctl restart
docker
+```
+
+Then, from a directory containing `build_test_engines.py`:
+
+```
+sudo docker run --rm --gpus all -v "$PWD:/w" -w /w
nvcr.io/nvidia/tensorrt:26.06-py3 bash -c "\
+ pip install -q --break-system-packages 'apache-beam[gcp]' cuda-python
pillow && \
+ python3 build_test_engines.py --dest gs://YOUR_BUCKET/models"
+```
+
+`--break-system-packages` is required because the container's Python
environment is
+marked externally managed. Credentials are picked up from the VM's service
account, so
+no extra authentication step is needed.
+
+The verification step imports the model handler from the installed
`apache-beam`, so
+that version has to support the TensorRT major version you are building for.
To verify
+against an unreleased change, copy your working tree's
+`apache_beam/ml/inference/tensorrt_inference.py` over the installed one inside
the
+container before running the script.
+
+Engines are written with a `_trt<major>` suffix, for example
+`single_tensor_features_engine_trt11.trt`, so the engines built by earlier
TensorRT
+versions stay in place and anyone on an older branch is unaffected.
+
+Point `--dest` at a bucket you can write to. Staging the results under
+`gs://apache-beam-ml/models/` is a separate, deliberate step for someone with
write
+access to that bucket; note it has no object versioning, so an overwrite
cannot be undone.
+
+Pass `--only <name>` to rebuild a single engine, and `--help` for the full
options.
diff --git
a/sdks/python/test-suites/containers/tensorrt_runinference/build_test_engines.py
b/sdks/python/test-suites/containers/tensorrt_runinference/build_test_engines.py
new file mode 100644
index 00000000000..606a74df398
--- /dev/null
+++
b/sdks/python/test-suites/containers/tensorrt_runinference/build_test_engines.py
@@ -0,0 +1,250 @@
+#
+# 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.
+#
+
+"""Rebuilds the TensorRT engines that the TensorRT tests load from GCS.
+
+A serialized TensorRT engine can only be deserialized by the TensorRT major
+version and GPU architecture that built it. The engines the tests load are
+therefore not portable, and have to be rebuilt whenever the TensorRT version
+in the test container changes or the tests move to a different GPU.
+
+This script rebuilds them from the ONNX sources already staged alongside them,
+and verifies each result by loading it back through the model handler the
+tests use. It cannot run as part of the test suite because it needs a GPU.
+
+Run it in the same container and on the same GPU type the tests use. See
+README.md in this directory for the exact commands. Requires TensorRT 10 or
+later; the engines built by TensorRT 8 are the ones being replaced.
+"""
+
+# pytype: skip-file
+
+import argparse
+import io
+import logging
+import os
+import sys
+import tempfile
+
+import numpy as np
+import tensorrt as trt
+
+TRT_LOGGER = trt.Logger(trt.Logger.INFO)
+TRT_MAJOR = int(trt.__version__.split('.')[0])
+
+SOURCE = 'gs://apache-beam-ml/models'
+COCO_IMAGES = [
+ 'gs://apache-beam-ml/datasets/coco/raw-data/val2017/000000289594.jpg',
+ 'gs://apache-beam-ml/datasets/coco/raw-data/val2017/000000000139.jpg',
+]
+
+
+def _copy(src, dst):
+ """Copies between any two paths Beam's FileSystems understands.
+
+ The TensorRT container has no gcloud CLI, but apache-beam[gcp] is installed
+ for the verification step anyway, so reuse it rather than shelling out.
+ """
+ from apache_beam.io.filesystems import FileSystems
+ logging.info('copy %s -> %s', src, dst)
+ with FileSystems.open(src) as fin, FileSystems.create(dst) as fout:
+ while True:
+ chunk = fin.read(8 << 20)
+ if not chunk:
+ break
+ fout.write(chunk)
+
+
+def build_engine(onnx_path, engine_path):
+ """Parses an ONNX file and serializes an engine for this GPU."""
+ # The SSD MobileNet ONNX contains an EfficientNMS_TRT node, so the bundled
+ # plugins have to be registered before the parser will accept it.
+ trt.init_libnvinfer_plugins(TRT_LOGGER, namespace="")
+
+ builder = trt.Builder(TRT_LOGGER)
+ # Explicit batch is the default from TensorRT 10 onwards, so no creation
+ # flags are needed. Engines are only ever rebuilt for the container the
+ # tests currently use, so there is no reason to support TensorRT 8 here.
+ network = builder.create_network()
+ parser = trt.OnnxParser(network, TRT_LOGGER)
+ with open(onnx_path, 'rb') as f:
+ if not parser.parse(f.read()):
+ for i in range(parser.num_errors):
+ logging.error(parser.get_error(i))
+ raise ValueError(f'Failed to parse {onnx_path}')
+
+ config = builder.create_builder_config()
+ plan = builder.build_serialized_network(network, config)
+ if plan is None:
+ raise RuntimeError(f'Engine build produced no plan for {onnx_path}')
+ with open(engine_path, 'wb') as f:
+ f.write(plan)
+ logging.info('built %s (%d bytes)', engine_path,
os.path.getsize(engine_path))
+
+
+def _handler(engine_path, batch_size):
+ from apache_beam.ml.inference.tensorrt_inference import (
+ TensorRTEngineHandlerNumPy)
+ return TensorRTEngineHandlerNumPy(
+ min_batch_size=batch_size,
+ max_batch_size=batch_size,
+ engine_path=engine_path)
+
+
+def verify_linear(engine_path, examples, expected):
+ """Checks a small linear engine against the values the unit tests assert."""
+ handler = _handler(engine_path, len(examples))
+ results = handler.run_inference(list(examples), handler.load_model())
+ actual = np.array([r.inference[0] for r in results]).reshape(-1)
+ if not np.allclose(actual, np.asarray(expected).reshape(-1), atol=1e-4):
+ raise AssertionError(f'{engine_path}: expected {expected}, got {actual}')
+ logging.info('verified %s -> %s', os.path.basename(engine_path), actual)
+
+
+def verify_ssd(engine_path):
+ """Runs the object detection engine on the images the Dataflow IT uses.
+
+ The outputs are checked for the shape and ordering the example's
+ PostProcessor indexes by, and for at least one confident detection.
+ """
+ from apache_beam.io.filesystems import FileSystems
+ from PIL import Image
+
+ handler = _handler(engine_path, 1)
+ engine = handler.load_model()
+
+ for image_path in COCO_IMAGES:
+ with FileSystems.open(image_path) as f:
+ image = Image.open(io.BytesIO(f.read())).convert('RGB')
+ # Mirrors preprocess_image() in the tensorrt_object_detection example.
+ image = image.resize((300, 300), resample=Image.Resampling.BILINEAR)
+ batch = [np.expand_dims(np.asarray(image, dtype=np.float32), axis=0)]
+
+ inference = list(handler.run_inference(batch, engine))[0].inference
+ if len(inference) != 4:
+ raise AssertionError(
+ f'{engine_path}: expected 4 outputs, got {len(inference)}')
+ _, boxes, scores, classes = inference
+ if boxes.shape[-1] != 4 or scores.shape != classes.shape:
+ raise AssertionError(
+ f'{engine_path}: unexpected output shapes; the engine tensor order '
+ f'must be num_detections, boxes, scores, classes. Got '
+ f'{[np.asarray(o).shape for o in inference]}')
+ if float(np.max(scores)) < 0.3:
+ raise AssertionError(
+ f'{engine_path}: no confident detection for {image_path}; top score '
+ f'was {float(np.max(scores)):.3f}')
+ logging.info(
+ 'verified %s on %s -> top score %.2f',
+ os.path.basename(engine_path),
+ os.path.basename(image_path),
+ float(np.max(scores)))
+
+
+# The inputs and outputs below mirror the constants in
+# apache_beam/ml/inference/tensorrt_inference_test.py, so a rebuilt engine is
+# checked against exactly what the tests will assert.
+SINGLE_EXAMPLES = [np.float32(v) for v in (1, 5, -3, 10)]
+SINGLE_EXPECTED = [2.5, 10.5, -5.5, 20.5] # y = 2x + 0.5
+
+MULTI_EXAMPLES = np.array([[1, 5], [3, 10], [-14, 0], [0.5, 0.5]],
+ dtype=np.float32)
+MULTI_EXPECTED = [17.5, 36.5, -27.5, 3.0] # y = 2*x0 + 3*x1 + 0.5
+
+
+def verify_single(engine_path):
+ verify_linear(engine_path, SINGLE_EXAMPLES, SINGLE_EXPECTED)
+
+
+def verify_multiple(engine_path):
+ verify_linear(engine_path, MULTI_EXAMPLES, MULTI_EXPECTED)
+
+
+# Each entry rebuilds one staged .trt file from its staged .onnx source.
+ENGINES = {
+ 'single_tensor_features_engine': {
+ 'onnx': 'single_tensor_features_model.onnx',
+ 'verify': verify_single,
+ },
+ 'multiple_tensor_features_engine': {
+ 'onnx': 'multiple_tensor_features_model.onnx',
+ 'verify': verify_multiple,
+ },
+ 'ssd_mobilenet_v2_320x320_coco17_tpu-8': {
+ 'onnx': 'ssd_mobilenet_v2_320x320_coco17_tpu-8.onnx',
+ 'verify': verify_ssd,
+ },
+}
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(
+ description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ '--dest',
+ required=True,
+ help='Where to write the rebuilt engines, e.g. gs://my-bucket/models or '
+ 'a local directory. Staging them under the shared bucket is a separate, '
+ 'deliberate step for someone with write access.')
+ parser.add_argument(
+ '--suffix',
+ default=None,
+ help='Name suffix for the rebuilt engines, so the engines built by '
+ 'earlier TensorRT versions can stay in place. Defaults to _trt<major>.')
+ parser.add_argument(
+ '--only',
+ action='append',
+ choices=sorted(ENGINES),
+ help='Rebuild only the named engine. May be repeated. Defaults to all.')
+ args = parser.parse_args(argv)
+
+ if args.dest.rstrip('/') == SOURCE:
+ parser.error(
+ f'Refusing to write to {SOURCE}. That bucket has no object '
+ 'versioning, so overwriting a staged engine could not be undone.')
+
+ suffix = args.suffix if args.suffix is not None else f'_trt{TRT_MAJOR}'
+ names = args.only or sorted(ENGINES)
+ logging.info(
+ 'TensorRT %s, suffix %r, building: %s',
+ trt.__version__,
+ suffix,
+ ', '.join(names))
+
+ written = []
+ with tempfile.TemporaryDirectory() as tmp:
+ for name in names:
+ spec = ENGINES[name]
+ onnx_local = os.path.join(tmp, spec['onnx'])
+ _copy(f'{SOURCE}/{spec["onnx"]}', onnx_local)
+
+ engine_local = os.path.join(tmp, f'{name}{suffix}.trt')
+ build_engine(onnx_local, engine_local)
+ spec['verify'](engine_local)
+
+ dest = f'{args.dest.rstrip("/")}/{os.path.basename(engine_local)}'
+ _copy(engine_local, dest)
+ written.append(dest)
+
+ print('\nRebuilt and verified with TensorRT %s:' % trt.__version__)
+ for dest in written:
+ print(f' {dest}')
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
+ sys.exit(main())
diff --git
a/sdks/python/test-suites/containers/tensorrt_runinference/tensor_rt.dockerfile
b/sdks/python/test-suites/containers/tensorrt_runinference/tensor_rt.dockerfile
index c1dc4deb6e6..0e86e79cd2d 100644
---
a/sdks/python/test-suites/containers/tensorrt_runinference/tensor_rt.dockerfile
+++
b/sdks/python/test-suites/containers/tensorrt_runinference/tensor_rt.dockerfile
@@ -14,15 +14,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ARG BUILD_IMAGE=nvcr.io/nvidia/tensorrt:23.05-py3
+# 26.06 ships TensorRT 11.0.0, CUDA 13.3 and Ubuntu 24.04 with Python 3.12.
+# TensorRT 8.x cannot target Blackwell GPUs such as the RTX Pro 6000 that
+# Dataflow now offers, so this image tracks the current TensorRT major version.
+# The Python version here must match the Beam SDK image copied in below.
+ARG BUILD_IMAGE=nvcr.io/nvidia/tensorrt:26.06-py3
+ARG BEAM_SDK_IMAGE=apache/beam_python3.12_sdk:latest
-FROM ${BUILD_IMAGE}
+FROM ${BEAM_SDK_IMAGE} AS beam_sdk
+
+FROM ${BUILD_IMAGE}
ENV PATH="/usr/src/tensorrt/bin:${PATH}"
WORKDIR /workspace
-COPY --from=apache/beam_python3.10_sdk:latest /opt/apache/beam /opt/apache/beam
+COPY --from=beam_sdk /opt/apache/beam /opt/apache/beam
RUN pip install --upgrade pip \
&& pip install torch>=1.7.1 \
@@ -32,4 +39,4 @@ RUN pip install --upgrade pip \
&& pip install cuda-python
ENTRYPOINT [ "/opt/apache/beam/boot" ]
-RUN apt-get update && apt-get install -y python3.10-venv
+RUN apt-get update && apt-get install -y python3.12-venv
diff --git a/sdks/python/test-suites/dataflow/common.gradle
b/sdks/python/test-suites/dataflow/common.gradle
index 7b64ace131e..4a21391d2fd 100644
--- a/sdks/python/test-suites/dataflow/common.gradle
+++ b/sdks/python/test-suites/dataflow/common.gradle
@@ -659,22 +659,23 @@ task mockAPITests {
}
// add all RunInference E2E tests that run on DataflowRunner
-// As of now, this test suite is enable in py310 suite as the base NVIDIA
image used for Tensor RT
-// contains Python 3.10.
// TODO: https://github.com/apache/beam/issues/22651
project.tasks.register("inferencePostCommitIT") {
dependsOn = [
- // TODO(https://github.com/apache/beam/issues/33078): restore the tensorRT
tests once the staged
- // model is fixed.
- // 'tensorRTtests',
'vertexAIInferenceTest',
'geminiInferenceTest',
'mockAPITests',
]
}
+// The base NVIDIA image used for TensorRT contains Python 3.12, so the
TensorRT
+// suite belongs here rather than in the py310 suite above.
project.tasks.register("inferencePostCommitITPy312") {
dependsOn = [
+ // TODO(https://github.com/apache/beam/issues/33078): restore the tensorRT
tests once the staged
+ // model is rebuilt. A serialized engine can only be read by the TensorRT
major version that
+ // built it, and the staged engine was built with TensorRT 8.x.
+ // 'tensorRTtests',
'vllmTests',
]
}
diff --git
a/website/www/site/content/en/documentation/ml/tensorrt-runinference.md
b/website/www/site/content/en/documentation/ml/tensorrt-runinference.md
index 4bae2d3ba7c..49ea6e0e088 100644
--- a/website/www/site/content/en/documentation/ml/tensorrt-runinference.md
+++ b/website/www/site/content/en/documentation/ml/tensorrt-runinference.md
@@ -66,7 +66,7 @@ trtexec --onnx=<path to onnx model> --saveEngine=<path to
save TensorRT engine>
To use `trtexec`, follow the steps in the blog post [Simplifying and
Accelerating Machine Learning Predictions in Apache Beam with NVIDIA
TensorRT](https://developer.nvidia.com/blog/simplifying-and-accelerating-machine-learning-predictions-in-apache-beam-with-nvidia-tensorrt/).
The post explains how to build a docker image from a DockerFile that can be
used for conversion. We use the following Docker file, which is similar to the
file used in the blog post:
```
-ARG BUILD_IMAGE=nvcr.io/nvidia/tensorrt:22.05-py3
+ARG BUILD_IMAGE=nvcr.io/nvidia/tensorrt:26.06-py3
FROM ${BUILD_IMAGE}
@@ -75,11 +75,11 @@ ENV PATH="/usr/src/tensorrt/bin:${PATH}"
WORKDIR /workspace
RUN apt-get update -y && apt-get install -y python3-venv
-RUN pip install --no-cache-dir apache-beam[gcp]==2.44.0
-COPY --from=apache/beam_python3.8_sdk:2.44.0 /opt/apache/beam /opt/apache/beam
+RUN pip install --no-cache-dir apache-beam[gcp]==2.76.0
+COPY --from=apache/beam_python3.12_sdk:2.76.0 /opt/apache/beam /opt/apache/beam
RUN pip install --upgrade pip \
- && pip install torch==1.13.1 \
+ && pip install torch==2.13.0 \
&& pip install torchvision>=0.8.2 \
&& pip install pillow>=8.0.0 \
&& pip install transformers>=4.18.0 \