The 8 GB Lab Book · Chapter 16

Keep the frame on the GPU side

Board
Jetson Orin Nano Super
Mode
15W
Setup
GStreamer 1.20
Summary
The CPU JPEG path, jpegenc then jpegdec, took 16.95 ms per 720p frame: 51% of the frame budget.

What I expected

Chapter 6 charged 2.27 ms for one u8 crossing plus GPU work. For Prior 1, I budgeted approximately a millisecond for each hop. That is near 3 ms for P2 (convert, cross) and near 2 for P3. For P4 I guessed 10 ms. A software JPEG decoder at approximately 100 Mpx/s (est.) needs 9 ms for 0.92 Mpx.

Prior 2 was that G_cam, which Chapter 1 left as a letter, would be in the tens of MB. That is a small number of RGBA frames in transit.

Prior 3 was that P3 is faster than P2 by one full crossing, two to three ms.

The theory

Bytes per frame, then a two-point line for each path

Bytes per frame (arithmetic)

720p RGBA: 1280 x 720 x 4 B = 3.69 MB
720p I420 source: x 1.5 B = 1.38 MB
480p RGBA: 640 x 480 x 4 = 1.23 MB
one 720p crossing at 68 GB/s: 0.054 ms
= bus time is nothing; Day 6 again

Two points make a line (720p vs 480p)

pixels: 0.922 M vs 0.307 M, a 3.0x step
P2: (5.73 - 1.70) / 0.614 Mpx = 6.6 ns/px
  fixed: 1.70 - 6.6 x 0.307 = -0.3 ms, so none
P4: (16.95 - 8.84) / 0.614 = 13.2 ns/px
  fixed: 8.84 - 13.2 x 0.307 = 4.8 ms
= P2 is pure per-pixel; JPEG has a 4.8 ms handshake

6.6 ns per 4-byte pixel is 0.6 GB/s. This rate suggests a per-pixel pass, not a DMA copy of a buffer. The 4.8 ms fixed part of JPEG is for each frame, and the size does not change it.

The crossing, priced (arithmetic from measured p50)

P2 - P3, 720p
1.04 ms
bytes not crossing
2.46 MB
crossing rate
2.4 GB/s
bus roof
68 GB/s
P3 does everything P2 does plus a resize, and moves 3.69 - 1.23 = 2.46 MB less across. It is 1.04 ms faster, so the crossing costs at least 0.42 ms per MB: 2.4 GB/s at best, 28x under the roof. Chapter 6's 'a hop is a wait', with a rate on it now.

The setup

Run (board, ~5 min)
python3 ~/day16_nvmm.py
python3 -c "import json;[print(p['name'],p['resolution'],round(p['latency_p50_ms'],2),round(p['latency_p99_ms'],2),round(p['g_cam_mb'],1)) for p in json.load(open('day16/results.json'))['pipelines']]"
With the C920
python3 ~/day16_nvmm.py --device /dev/video0
# MJPEG mode, the line from the script's notes:
# v4l2src device=/dev/video0 io-mode=2 ! image/jpeg,width=1280,height=720,framerate=30/1 ! jpegdec ! video/x-raw,format=I420 ! nvvidconv ! ...
# a UVC camera lands frames in system memory; it cannot hand nvvidconv an NVMM buffer, so P2 is its best case
From results.json, the file the run wrote
"meta": { "script": "day16_nvmm.py", "res_flag": "both", "warmup": 60, "frames": 600 }
"pipelines": [ ... ]
"notes": [ ... ]

The measurement

WhatExpectedMeasuredNote
P1 raw system memory (no conversion) @ 720p-p50 0.17 / p99 0.67 ms30 fps, pool 5 MB
P2 NVMM convert, then back to system @ 720p-p50 5.73 / p99 6.44 ms30 fps, pool 27 MB
P3 stay in NVMM, resize there, small buf @ 720p-p50 4.69 / p99 5.00 ms30 fps, pool 8 MB
P4 JPEG encode/decode (the anti-pattern) @ 720p-p50 16.95 / p99 19.20 ms30 fps, pool 2 MB
P1 raw system memory (no conversion) @ 480p-p50 0.12 / p99 0.68 ms30 fps, pool 1 MB
P2 NVMM convert, then back to system @ 480p-p50 1.70 / p99 1.84 ms30 fps, pool 1 MB
P3 stay in NVMM, resize there, small buf @ 480p-p50 1.74 / p99 1.87 ms30 fps, pool 0 MB
P4 JPEG encode/decode (the anti-pattern) @ 480p-p50 8.84 / p99 10.35 ms30 fps, pool 3 MB
Measured on the board unless the row says estimate or predicted.
Four GStreamer pipelines, synthetic 720p30 and 480p30 sources: latency p50 and p99 (ms)
Fig 16.1Four GStreamer pipelines, synthetic 720p30 and 480p30 sources: latency p50 and p99 (ms).

Memory type on every pad; circle the one crossing

  1. Source[system]
  2. NvvidconvRGBA [NVMM]
  3. Resize480p [NVMM]
  4. One small buffer[system]
Then appsink. Last box: the only place the CPU touches a frame. P2 is this drawing without the resize box, with a full 720p buffer crossing instead.
P1 raw
0.17
P2 NVMM, back
5.73
P3 NVMM, resize
4.69
P4 JPEG
16.95

Line at 33.3 ms

p50 per 720p frame, ms, 600 frames after 60 warm. The dashed line is one frame at 30 fps: P4 spends 51% of it, P3 14%. P1 is the no-work floor, not a result. Tails (p99 - p50): P1 0.50, P2 0.71, P3 0.31, P4 2.25 ms; the path with the fewest CPU touches has the tightest tail, and JPEG's tail alone is half of P3.
pathp50 msp99 mspool MB
P1 raw0.120.681
P2 NVMM, back1.701.841
P3 NVMM, resize1.741.870
P4 JPEG8.8410.353
480p, all four paths (measured).

Where it broke

For Prior 1, P2 at 5.73 is double my 3 ms. The error is for each pixel, not for each hop. A convert-and-cross costs 6.6 ns for each pixel at all sizes. P4 at 16.95 was near my 10 only by chance. 4.8 ms of it, 28 percent, is a fixed cost, and I had no model for that part. At 480p that fixed part is 54 percent of 8.84.

For Prior 3, the result was 1.04 ms, not 2 to 3. The resize also has a cost, so the saving is a crossing minus a resize. There is a check that this model is correct. At 480p there is nothing to resize, so P3 should be equal to P2. The result is 1.74 against 1.70. The two values agree.

Prior 2 was correct, but only by a small margin. 27 MB / 3.69 MB = 7.3, so the pool of P2 holds seven RGBA frames. I read the 8 MB of P3 as one 720p frame plus three small frames.

I modelled hops again, after Chapter 6 told me to model waits. The two-resolution line is the tool that I should have used first. One subtraction separates the per-pixel cost from the per-frame cost. The price is one extra run.

The source was a test pattern, not a sensor, so 4.69 ms is the cost of the pipeline alone. A C920 adds a USB transfer and a YUY2 unpack or a CPU jpegdec, and none of these four paths contain them.

results.json records the pipeline string of P4: jpegenc, then jpegdec. The two are software elements on the CPU. The 4.8 ms fixed cost is therefore CPU software, not a hardware block. P4 also paid for an encode, not only a decode.

Summary

  • NVMM is not faster memory. It is the same LPDDR5. With NVMM, there is no CPU color pass and no copy on each frame. P3 is faster than P2 because the resize occurs before the buffer crosses. The buffer that crosses is small.
  • The JPEG path is the cost of a "read frames from files" demo at 30 fps. It uses 51 percent of the frame budget for a CPU encode and decode. 4.8 ms of it (28 percent) is a fixed cost for each frame. At 480p the fixed part is 54 percent of 8.84 ms.
  • The pool numbers are the G_cam term from Chapter 1, which now has a measured value. They are small: single-digit to low-double-digit MB. The cost of the camera is latency, not RAM.
  • A USB UVC camera like the C920 delivers YUY2 or MJPEG to system memory. P2 is its predicted best case. P4 is an upper limit for its MJPEG case, because that case needs only the decode. CSI through Argus can use P3.

Sources

These sources are not mine. Measured numbers came off my own board; specs, estimates and paper figures are labeled as such.

  • NVIDIA NvBufSurface API
  • Jetson CSI camera guide
  • DeepStream buffer management
  • GStreamer nvvidconv docs
  • USB vs CSI distinction in Jetson camera architecture