TensorRT Custom Plugin Produces Non-Deterministic Results with Identical Inputs Across 20 Iterations

Description

I’m experiencing a non-deterministic inference issue with my TensorRT custom plugins. When running the same plugin 20 times with identical inputs, the output results are sometimes inconsistent (some iterations pass validation, others fail).

Environment

TensorRT Version:10.16
GPU Type:RTX 4060 Laptop
Nvidia Driver Version:561.17
CUDA Version:12.6
CUDNN Version:9.8
Operating System + Version:windows 10 +vs2019 +vc14
Python Version (if applicable):not python,use vc14
TensorFlow Version (if applicable):
PyTorch Version (if applicable):
Baremetal or Container (if container which image + tag):

Relevant Files

Source code can be found on GitHub - xunge817/TensorRT_test · GitHub .

Steps To Reproduce

My network runs 20 iterations of the same decoder layer with identical inputs:

Input tensors: reference_boxes (constant) and valid_ratios = [1.0, 1.0, 1.0, 1.0]
Each iteration uses custom plugins: Formula1 and PositionEmbeddingSine
3 debug outputs per iteration (60 total)

When comparing TensorRT outputs against Python ground truth (threshold = 0.01):

Some iterations PASS, others FAIL
The same plugin produces different results across iterations despite identical inputs
Inconsistent behavior: rerunning the program may produce different pass/fail patterns

right answer

wrong answer

Hi @505010579, thanks for the clean repro and the public GitHub link, that makes this much faster to triage.

Non-determinism on a custom plugin with identical inputs is almost always one of four things, in roughly this order of likelihood:

1. Uninitialized output or workspace memory

If your plugin’s enqueue writes to only part of the output tensor (e.g. only the valid positions in a sparse layout) and trusts that the rest is “don’t care”, the unwritten cells take whatever was previously in that GPU memory region. Across 20 iterations of the same engine, the workspace pool may hand the same allocation back with different residue each time, so the comparison against ground truth flickers.

Fix: zero the output explicitly at the start of enqueue:


cudaMemsetAsync(outputs[0], 0, output_bytes, stream);

This will catch it immediately. If the determinism returns once you memset, the bug is in your kernel’s coverage of the output tensor.

Easiest way to confirm before you patch: run with compute-sanitizer --tool initcheck and see if it flags reads of uninitialized memory.

2. Stream / synchronization mismatch

If your plugin internally launches kernels on a different stream than the one TRT passed to enqueue, or if it does CPU work that depends on a value not yet computed on the GPU, you get a race. Same inputs but the race resolves differently between runs.

Concretely:

  • Every kernel and cudaMemcpyAsync inside your plugin must use the stream argument from enqueue, not the default stream.

  • If you must use a side stream, insert cudaEventRecord + cudaStreamWaitEvent to make TRT’s stream wait for it.

  • Don’t issue blocking copies (cudaMemcpy) or cudaDeviceSynchronize from inside enqueue for production, but as a debugging step, putting a cudaStreamSynchronize(stream) at the top and bottom of enqueue will tell you fast whether the bug is async-related (determinism returns) or kernel-internal (still non-deterministic).

3. Atomic reductions / unstable reduction order

PositionEmbeddingSine is usually a deterministic compute, but if Formula1 or any helper kernel uses atomicAdd for a reduction or does a parallel reduce without a fixed order, the floating-point result depends on the order in which threads commit. That can drift across runs at the threshold you’re using (0.01).

Fix: use a deterministic reduction (single-block tree reduction, or a two-pass scheme). If the residual is below ~1e-6 then it’s just FP non-associativity, but at 0.01 threshold that’s unlikely to be the whole explanation, so check #1 and #2 first.

4. State carried across enqueue calls

If your plugin caches a workspace pointer or a buffer across enqueue calls (member variable on the plugin object) and TRT recycles workspace memory between iterations, you may end up reading stale data. The V3 plugin contract treats every enqueue as independent - any per-iteration state must be re-derived inside enqueue, not cached on this.

Reference patterns

For comparison against a known-good V3 plugin layout (especially for the workspace request and stream handling), the OSS plugins are the canonical reference: TensorRT/plugin at main · NVIDIA/TensorRT · GitHub

If you can run compute-sanitizer --tool initcheck and compute-sanitizer --tool racecheck against your repro and paste the output here (even just the first few warnings), that’ll usually pin the cause immediately and we can take it from there.

Thanks, Atharva