Keep Jetson video pipelines tensor-native - #2759
Draft
alexnorell wants to merge 255 commits into
Draft
Conversation
Add an optional torch.Tensor representation to WorkflowImageData with
lazy BGR<->RGB conversion between the two backings. Layout contract:
numpy is HWC uint8 BGR (cv2 native), tensor is HWC uint8 RGB
(inference-models / torch convention). dtype is preserved; no implicit
device moves.
- __init__ accepts tensor_image; "empty" check covers the new field.
- numpy_image property: if only tensor is set, materialize via
detach().to("cpu").numpy() with a channel flip and cache the result.
- tensor_image property: mirror fallback from numpy with channel flip.
- copy_and_replace propagates tensor_image.
- create_crop_from_tensor: tensor-native sibling of create_crop with
identical metadata math.
- _read_shape_without_materialization avoids forcing device->host just
to fill parent_metadata / workflow_root_ancestor_metadata origin
coordinates when only the tensor representation is set.
Public surface is unchanged; the field is opt-in. Serialization (via
base64_image -> numpy_image -> JPEG) continues to work transparently.
New common/deserializers_tensor.py and common/serializers_tensor.py.
The numpy files are untouched per the plan's locked [ITERATE 4.A]
decision; the tensor file's deserialize_image_kind handles raw
torch.Tensor input and the dict-shape {"type": "tensor", "value": ...}
input, and delegates all other inputs (np.ndarray, base64, URL, dict
with type=base64/url) to the numpy implementation. The serializer
sibling currently re-exports the numpy functions because the lazy
tensor->numpy fallback in WorkflowImageData makes serialise_image
correct in both modes; the file exists to keep the loader's import
swap symmetric and to give future tensor-aware optimisations a
landing spot without touching the loader contract.
Mirror module per the plan's Step 5b. attach_parents_coordinates_* helpers go through ImageParentMetadata.origin_coordinates, which is populated by WorkflowImageData.parent_metadata / workflow_root_ancestor_metadata via _read_shape_without_materialization, so the tensor mirrors currently delegate to the numpy implementations. The module exists so future tensor-specific divergence has a landing spot the loader can swap to without touching the numpy file.
…path map_inference_kwargs unconditionally sets input_color_format="bgr", which is the right default for the cv2-derived numpy paths (preprocess / predict / postprocess) but breaks the new run_tensor_native_inference entry points: workflows tensor blocks pass RGB tensors per the workflows tensor-data-representation plan and need a way to opt out of the BGR override. In each of the four adapter classes that implement run_tensor_native_inference (object detection, instance segmentation, classification, semantic segmentation), pop input_color_format from kwargs before map_inference_kwargs (default None), then restore it afterwards. map_inference_kwargs itself is untouched, so every old execution path keeps the BGR default it has today. Effect: callers of run_tensor_native_inference can pass input_color_format="rgb" (or "bgr", or leave it None) and the value travels through to the underlying model unchanged.
New v3_tensor.py next to v3.py. Per the plan's locked decisions: - Manifest is verbatim (same type literal, name, version, description, fields, outputs, ui_manifest). Class name unchanged so the loader's if/else swap binds the same identifier in both branches. - run_locally calls model_manager.run_tensor_native_inference with the per-image torch tensors from WorkflowImageData.tensor_image and passes input_color_format="rgb" (adapter now respects caller's value). Skips convert_inference_detections_batch_to_sv_detections because the adapter returns sv.Detections directly. - attach_parents_coordinates_to_batch_of_sv_detections_tensor used on the local path; numpy mirror reused on the remote path per [ITERATE 6.A]. - run_remotely materialises base64_image (which lazily goes tensor -> numpy -> JPEG) and hits the same HTTP API as v3.py. Remote response is dict-shaped so the numpy converter applies. - inference_id read from sv.Detections.data when present; uuid4 fallback per image per [ITERATE 6.B].
Wire the three pieces the plan's Step 3 calls for, gated on
ENABLE_TENSOR_DATA_REPRESENTATION:
- Import env flag.
- Move serialise_image / serialise_sv_detections / serialise_rle_sv_detections
and deserialize_image_kind / deserialize_detections_kind /
deserialize_rle_detections_kind into an if/else swap that picks the
_tensor module when the flag is on. All other (de)serializer
functions stay imported from the numpy file (the tensor file only
mirrors the image/detection trio that has tensor-aware behaviour).
- Object-detection V3 block import becomes an if/else that picks
v3_tensor.py when the flag is on. Class name unchanged, so the
blocks = [...] list and load_blocks() are untouched.
load_kinds() and KINDS_SERIALIZERS / KINDS_DESERIALIZERS dict
construction are unchanged per the plan ("no new kinds").
…eline) (#2647) * feat(workflows): GPU-native mask visualization compositor for tensor pipeline Replaces the sv.MaskAnnotator path in the tensor-native mask visualization block with a torch CUDA compositor when the mask carrier is a dense CUDA bool tensor (CLASS/INDEX color lookups), cutting the viz step from ~210 ms to ~23 ms on Jetson Orin Nano at 1080p with 15 detections. Output is validated 100% pixel-identical to sv.MaskAnnotator (paint order, tie-breaking, and cv2.addWeighted rounding); any ineligible input or GPU-path error falls back to the unchanged sv path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(workflows): argmin ownership + tensor-only scene in GPU mask compositor - Replace the per-instance paint loop (argsort + N masked_fill_ kernels) with a single argmin over area-weighted masks: smallest-area mask owns each pixel, argmin's first-index tie rule matches supervision's stable flip-sort ties, so output stays bit-identical to sv.MaskAnnotator. Jetson Orin Nano 1080p/15 masks: 19.3 -> 14.9 ms kernel median. - Drop the numpy-scene branch in run(): the tensor pipeline always carries a CHW RGB device tensor (HTTP base64 ingest is being converted to tensor at pipeline start as well). - Remove the inert InstancesRLEMasks crop side-channel (getattr fields that never existed); RLE-carrier predictions take the sv fallback. - Tests: drop _dense_crops_and_offsets cases (helper deleted); 18 pass on Jetson GPU, 7 pass / 11 skipped on CPU CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Support RLE mask carrier and switch overlaps to blend-all in GPU mask compositor Addresses the PR review: predictions.mask can be InstancesRLEMasks (the tensor pipeline's default via SAM3), not only a dense (N, H, W) bool tensor. The RLE carrier is now decoded natively — vectorised numpy varint decode of COCO counts, one packed H2D of per-run tables, and a device-side run→pixel scatter — so the dense stack is never materialised (O(foreground pixels) instead of O(N·H·W)). Overlap semantics change intentionally: overlapping masks are blended (mean of covering masks' premultiplied colors), replacing the smallest-area-owns-the-pixel parity machinery. This is order-independent, simpler, and measured ~30% faster with ~3x lower peak CUDA memory than the argmin ownership path on Orin Nano (30 detections: 19.7 ms vs 28.6 ms @ 1080p, 175 vs 524 MiB). Pixels covered by a single mask still match sv.MaskAnnotator bit-for-bit. Also hardened: mask-canvas/scene size mismatch now raises into the sv fallback instead of painting misaligned, RLE payload count must match boxes, CLASS lookup requires class_id, and the GPU path is gated on image.is_tensor_materialised() so numpy-sourced frames never pay a forced CHW tensor materialisation. Verified on Jetson Orin Nano (jetson-6.2.0 container): 49/49 tests pass on CUDA; decoder fuzzed against pycocotools (300 cases). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the GPU mask compositor tensor-only gpu_mask_composite doubled as a numpy drop-in for sv.MaskAnnotator.annotate (return_tensor / scene_layout flags, HWC-BGR numpy upload/download branches, internal BGR->RGB LUT flip) — a test-harness API the production block never used. The compositor now takes exactly the tensor-pipeline contract: CHW RGB uint8 device tensor in, the same tensor returned mutated in place, colors passed as RGB (the block resolves the sv palette with .as_rgb()). Numpy-based parity tests go through a small _composite_bgr adapter in the test file, where the conversion belongs. Re-verified on Jetson Orin Nano: 49/49 tests pass on CUDA; dense 1080p median 19.68 ms / 175 MiB peak — identical to before the refactor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Support TRACK color lookup in the GPU mask compositor TRACK never needed the sv path: sv's resolve_color_idx is just palette.by_idx(tracker_id), and the tensor pipeline carries tracker ids in bboxes_metadata — the same place to_supervision_for_annotation reads them from. _resolve_color_ids now resolves palette indices for CLASS/INDEX/TRACK and raises sv's exact ValueErrors when class or tracker ids are missing, BEFORE any mask work — previously a doomed run would densify RLE masks on the sv fallback path just to crash on the same check. Verified on Jetson Orin Nano: 53/53 tests pass on CUDA, including bit-exact TRACK color parity vs sv.MaskAnnotator(color_lookup=TRACK) with non-contiguous tracker ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Paweł Pęczek <146137186+PawelPeczek-Roboflow@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
InferencePipelinefactory without changing legacy positional argument orderRoot causes
Geary connected to
nvv4l2decoder, but RTSP began mid-GOP and supplied no DTS. During the observed startup window every input was a delta frame; the decoder never established a reference picture, and subsequent B-frame timestamps causedGstVideoDecoderto discard output. The H.264 pipeline now requests a keyframe at the depayloader and reconstructs DTS before NVDEC. GStreamer 1.24 does not expose those keyframe properties onrtph265depay, so H.265 receives the timestamper without invalid launch properties.UniFi also needed source-scoped TLS configuration and SDES-SRTP caps normalization before
srtpdec.The first source-option API was present only on workflow/custom factories. The primary
InferencePipeline.init()could not forward it, and its dictionary-only annotation contradicted the already-supported aligned multi-source list. All factories now shareVideoSourceOptionsand the primary factory propagates it. The new option is trailing on every public factory and source helper so existing positional callers retain their exact pre-change binding.The JP62 Dockerfile intentionally installs a pinned TensorRT local-repository package, but a clean checkout did not contain or fetch that generated multi-GB artifact.
docker/scripts/build_jetson_6_2_image.shnow invokes the shared fetcher first. The fetcher uses pinned URL/SHA defaults, bounded retries/timeouts, verified cache reuse, an atomic temporary file, Linux/macOS checksum tools, and cleanup on failure. CI calls the same fetcher.Validation
h264timestamperandh265timestamper(3, 1080, 1920), NVMM frame, one CUDA conversion kernel, zero host-to-device copies, zero device-to-host copiesJetsonVideoFrameProducerand remain running without OpenCV fallbackYOLOv8ForObjectDetectionTRT)git diff --checkpassroboflow/roboflow-inference-server-jetson-6.2.0:1.4.0-tensor-exp-33d9c65cf324278d226ff22fc0487c7c03fb7e2095sha256:7a19b84954bdd0a7c146a817e47d3f2d992f4ace35910c935a0de9bfd0534ed9Dependency
The stacked roboflow-edge PR #605 pins this JP62 image, enables tensor output only for JP62, emits tensors from supported industrial cameras, and defers device-to-host materialization until an explicit CPU consumer.