Build1 publisher3 min readPublished
A whole-seconds cast zeroes the parse term in NVIDIA's JPEG2000 decode sample
The sample times the GPU decode correctly with CUDA events, then adds a Tier-2 parse term that a cast to whole seconds rounds to zero on every frame. Fastvideo puts that missing stage at 15 to 29% of decode time.
The Engineer · Build desk

What happened
- NVIDIA's documentation calls Tier-2 the first stage of decoding, and in Fastvideo's stage table that stage takes 15% of decode frame time at 2K and 29% at 4K.
- The decode sample runs one frame at a time with a single decoder state and a single job queue, and its -b flag groups only the reading of files from disk.
- Fastvideo published its nvJPEG2000 figures from a separate program whose timer starts before nvjpeg2kStreamParse and stops after the GPU finishes decoding.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A table that sets the sample's printed decode time beside a whole-pipeline figure flatters the sample side, so anyone comparing GPU JPEG2000 codecs has to re-time both under one rule before the columns mean anything.
- decision Two questions now precede reading any GPU codec throughput number: where the timer opens, and how many frames are in flight, because a flag named like a batch size may only be batching disk reads.
- exposure The zeroed term lives in sample code that teams copy as a starting harness, so timing code derived from nvjpeg2000DecodeSample.cpp inherits a parse cost of zero without warning.
- cost Checking the size of the omission for NVIDIA's library is not possible from its own output, so a buyer who wants comparable numbers pays for an in-house benchmark program instead.
The two terms in the decode sample's total come from different clocks. The GPU work is timed with a pair of CUDA events recorded on the same queue, one before nvjpeg2kDecodeImage and one after [1]. That is correct, because the call is asynchronous: it puts the work into a GPU job queue and returns immediately, so a CPU stopwatch around it would time the enqueue [2]. Tier-2 parsing, done by nvjpeg2kStreamParse, is timed on the CPU with perfclock::now() and added to the total afterwards [3].
The addition is the defect. The sample casts the interval with duration_cast<std::chrono::seconds> and takes count(), so the value is truncated to whole seconds [4]. Parsing a frame takes milliseconds [5]. Whole seconds is a generous unit for a millisecond. So time += parse_time adds exactly zero, for every frame on every machine [6]. Fastvideo's write-up says: "This is not a choice of measurement boundary; it is an error in the measurement." [7]
NVIDIA's documentation calls Tier-2 the first stage of decoding [8]. In Fastvideo's own stage table, Tier-2 at decoding takes 15% of frame time at 2K and 29% at 4K [9]. Those shares are fvJPEG2000's, because nvJPEG2000 does not report time by stage and its shares are unknown to Fastvideo [10]. If they transferred, a total missing that stage would be 71% to 85% of whole-decode time, and whole decode would take 1.18 to 1.41 times the printed number [22]. For the transfer to hold, Tier-2 in nvJPEG2000 would have to cost about what it costs in fvJPEG2000 on the same CPU; Fastvideo argues the stage is the same and runs on the CPU in the same way [11].
Other work sits outside the timed region: allocating GPU buffers, reading the file and writing the result [12]. The decoded frame is never copied back to host memory inside the measured loop [12].
Concurrency in the sample is one frame deep. It keeps one decoder state and one queue of GPU jobs, and waits after every frame [13]. The -b option is described as a batch size and groups only the reading of files from disk [14]. In Fastvideo's tables the notation 8x2 means eight CPU threads with two frames in flight on the GPU in each [15], and the write-up warns that batching works differently in the two codecs, so the same word in the tables would otherwise mean two things [25].
The encoding sample draws its boundary differently, and by Fastvideo's account correctly. The whole per-frame loop is inside the timer, including nvjpeg2kEncodeRetrieveBitstream, which copies the compressed image to host memory; loading the source frame onto the GPU stays outside, because it happens when the file is read [16]. Fastvideo says that is the boundary it uses in single-frame mode [17].
For its published nvJPEG2000 numbers, Fastvideo did not use the sample. It wrote bench/nvj2k_bench-02/nvj2k_bench-02.cpp, whose timer opens before nvjpeg2kStreamParse and closes after the GPU has finished decoding, so Tier-2 is inside the measured time on both sides [18]. The harness, the scripts and the raw logs are on GitHub [19]. The author of the write-up works at Fastvideo, which makes the other codec in the comparison [20].
What to watch
- Whether NVIDIA changes the duration_cast in nvjpeg2000DecodeSample.cpp or the total the sample prints.
- Per-stage timing in nvJPEG2000 would let someone check whether Tier-2 costs there what it costs in fvJPEG2000.
- Whether anyone outside Fastvideo reruns the published harness on other hardware and posts the logs.