Skip to content

Support configurable mask grids in RF-DETR Triton postprocessing - #3154

Merged
dkosowski87 merged 56 commits into
mainfrom
codex/rfdetr-triton-mask-tradeoff
Oct 9, 2026
Merged

dkosowski87 merged 56 commits into
mainfrom
codex/rfdetr-triton-mask-tradeoff

Conversation

@dkosowski87

@dkosowski87 dkosowski87 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

RF-DETR's Triton RLE postprocessor rejects mask-resolution factors other than 1.0, forcing reduced masks through the per-instance PyTorch/RLE loop. This change supports native and upsampled grids in the sparse Triton path, retaining reference fallback for antialiased downsampling and unsupported transforms.

Aligned with merged #3156: boxes and masks share the final mask_size grid inside inference_models. Both selection kernels map normalized boxes through network dimensions directly to that grid, preserving reference arithmetic and dtype boundaries. The shared finalizer retains float32 fractional boxes when the actual mask and image grids differ; equal grids use final integer rounding. Eager, multiclass, deferred, and empty results follow the same convention. Original frame metadata, HTTP opt-in behavior, and to_supervision() restoration retain the merged contract.

Global top-k precedes class remapping, with one sparse mask per selected query and no four-class cutoff. Per-query retry flags avoid the shared-header race. Packed interpolation tables preserve reference coefficients, and kernels read mask strides without copying every query. Scope is the Triton postprocessor, its tests, and documentation; the existing enablement flag controls it. Profiling uses implementation-selection-mode=base, with no execution-plan or model-forward change.

Exact snapshots at factor 0.25 match all 417 detections, including box dtypes/values and every mask pixel. At that factor, warm complete-inference latency improves, but cold first use and first-process RSS regress. This remains a draft and does not satisfy unconditional no-regression acceptance.

In the results below, factor means masks_resolution_factor, the model parameter selected from HTTP tradeoff_factor when tradeoff mode and reduced-mask opt-in are enabled. All results use RLE masks. The existing measurements cover:

Factor Measured comparison Scope
0.25 Non-Triton reference/fallback → current Triton Complete-inference latency, fresh-process RSS/first use, snapshots, Nsight; also paired postprocessing-only timings
0.5 and 0.0 Non-Triton reference → current Triton Paired warm postprocessing-only latency and incremental CUDA allocation; no complete-inference campaign at these factors
1.0 Previous Triton implementation → current Triton Paired warm postprocessing-only latency and incremental CUDA allocation; no timed Triton-disabled baseline at 1.0

The complete-inference campaign below uses factor 0.25 throughout. Its baseline falls back to the non-Triton path at this factor. Base is 640×480/batch 1/confidence 0.4; large is 1920×1080/batch 4/confidence 0.01. Large timings are per four-image batch.

Factor Workload Metric Baseline Candidate Change Change % Criterion Status Evidence
0.25 base warm median (ms) 62.996 55.117 -7.879 -12.51% ≤ baseline pass B/A artifacts/base/latency.json
0.25 base warm p95 (ms) 67.068 58.025 -9.044 -13.48% ≤ baseline pass B/A artifacts/base/latency.json
0.25 base first-use rep 1 (s) 1.441 10.109 +8.668 +601.63% ≤ baseline fail B/A artifacts/base/memory.json
0.25 base process HWM rep 1 (MiB) 1575.441 1612.691 +37.250 +2.36% ≤ baseline fail B/A artifacts/base/memory.json
0.25 large warm median (ms) 353.308 211.384 -141.923 -40.17% ≤ baseline pass B/A artifacts/large/latency.json
0.25 large warm p95 (ms) 363.296 218.427 -144.869 -39.88% ≤ baseline pass B/A artifacts/large/latency.json
0.25 large first-use rep 1 (s) 1.744 10.978 +9.234 +529.37% ≤ baseline fail B/A artifacts/large/memory.json
0.25 large process HWM rep 1 (MiB) 1612.855 1632.602 +19.746 +1.22% ≤ baseline fail B/A artifacts/large/memory.json

At factor 0.25, first-use measurements are complete first inference calls in fresh processes, not isolated compiler timers. Later processes reuse Triton's disk cache. Warmup moves this cost before serving; it does not remove it.

Memory, full-resolution control, and trace details

Independent process RSS peaks and first-use times below are all at factor 0.25, with the same reference/fallback baseline as the complete-inference table. Peaks are retained individually:

Factor Workload Baseline HWM, repetitions 1/2/3 (MiB) Candidate HWM, repetitions 1/2/3 (MiB) Baseline first-use seconds, 1/2/3 Candidate first-use seconds, 1/2/3
0.25 base 1575.441, 1539.113, 1542.523 1612.691, 1523.117, 1519.422 1.441, 0.486, 0.491 10.109, 0.639, 0.619
0.25 large 1612.855, 1578.305, 1576.793 1632.602, 1551.238, 1551.965 1.744, 0.774, 0.762 10.978, 0.790, 0.786

The paired raw-output benchmark measures postprocessing only, on identical raw model outputs. Both workloads here use confidence 0.4 and OpenCV-resized inputs: base is 640×480/batch 1, large is 1920×1080/batch 4. Its large workload differs from the complete-inference campaign's confidence 0.01; do not combine their stage and end-to-end timings. There are 20 warmups and five alternating rounds of 50 calls per implementation.

For factors 0.0, 0.25, and 0.5, the timed control directly calls the non-Triton reference implementation, the same path used when Triton postprocessing is disabled. The mode column describes current Triton; the reference control uses its ordinary synchronous implementation in both comparisons. All entries below are warm measurements; negative changes indicate lower latency.

Factor Workload Current Triton mode Non-Triton median / p95 (ms) Current Triton median / p95 (ms) Median change Extra CUDA allocation peak, reference → current (KiB)
0.5 base eager 9.807 / 10.989 3.212 / 3.790 -67.25% 1468.5 → 393
0.5 base deferred 9.725 / 10.586 3.217 / 4.101 -66.92% 1468.5 → 401
0.5 large eager 19.208 / 20.336 11.158 / 12.791 -41.91% 6591.5 → 393
0.5 large deferred 19.151 / 20.817 11.074 / 13.395 -42.18% 6591.5 → 401
0.25 base eager 9.735 / 10.838 3.008 / 3.643 -69.10% 808 → 393
0.25 base deferred 9.597 / 10.441 2.922 / 3.540 -69.55% 808 → 401
0.25 large eager 18.466 / 20.108 8.944 / 10.524 -51.57% 2353.5 → 393
0.25 large deferred 18.465 / 20.107 8.848 / 11.320 -52.08% 2353.5 → 401
0.0 base eager 8.554 / 9.362 2.841 / 3.326 -66.78% 455.5 → 393
0.0 base deferred 8.450 / 9.236 2.823 / 3.521 -66.59% 455.5 → 401
0.0 large eager 16.399 / 17.760 6.870 / 8.039 -58.11% 431.5 → 393
0.0 large deferred 16.136 / 17.572 7.001 / 8.752 -56.62% 431.5 → 401

Evidence: rfdetr-mask-coords-benchmark-afcae76a-20261009/output/postprocess-benchmark.json. Exact parity passes in all 16 combinations, including factor 1.0 with the different control described next. Retained allocator growth is zero throughout. Allocation figures are incremental CUDA tensor peaks, not total process GPU memory; NVML process accounting is unavailable on this Jetson, and shared host/GPU RAM channels must not be added.

At factor 1.0, the first full-grid postprocessing-only comparison showed median increases of 0.4–1.6%, within repetition spread. A longer repeat (10 alternating rounds × 100 calls per implementation, after 20 warmups) follows. Control is the previous Triton implementation, at 5cfa38595219a736be92cd0d6948f32bd1e9c123, with Triton enabled. These results do not measure improvement over Triton disabled at 1.0; no such timed baseline was collected. Both implementations have the same incremental CUDA allocation peaks: 393 KiB eager / 401 KiB deferred. Both sizes use confidence .4 and OpenCV-resized inputs here; this differs from the large campaign's .01 workload.

Factor Full-grid workload Mode Control median / p95 (ms) Candidate median / p95 (ms) Median change
1.0 base eager 3.751 / 4.283 3.760 / 4.398 +0.23%
1.0 base deferred 3.635 / 4.394 3.647 / 4.341 +0.33%
1.0 large eager 14.765 / 16.365 14.899 / 16.597 +0.91%
1.0 large deferred 14.975 / 16.712 15.085 / 16.927 +0.73%

Nsight CUDA/NVTX evidence at factor 0.25, reference/fallback → current Triton, per inference (or batch):

Factor Workload Postprocess kernels, before → after Synchronizations, before → after Forward kernels, before → after
0.25 base 158 → 8 58 → 2 641 → 641
0.25 large 2953 → 65 1228 → 12 670 → 670

The sparse RLE kernels execute in both workloads, replacing the reference per-mask resize/copy loop. Trace timing includes instrumentation and is not used as standalone latency. CPU sampling is unsupported; CUDA/NVTX capture remains valid. All five complete-inference latency repetitions at factor 0.25 are retained. Host telemetry covers every selected repetition with no diagnostics or reported throttle events. CPU clocks stay at 2201 MHz; GPU clock metrics are unavailable. GPU temperature peaks are 52.03→52.59°C/base and 55.25→55.44°C/large. Median CPU/CV power is 7.34→7.32 W/base and 6.96→7.31 W/large; GPU/SoC power is 5.80→5.80 W/base and 13.09→14.24 W/large. These are named Jetson rails, not isolated GPU power.

Validation (all 80 current-head PR checks pass):

  • 292 targeted tests pass on Orin across two runs with identical runtime code: 220 at 884260f93, then 72 coordinate cases at afcae76a1. The first run's 72 failures were an immutable-metadata fixture assignment, fixed with _replace; no runtime code, expected values, or tolerances changed. Tests cover fp32/fp16/bfloat16, network resize and nonsquare dimensions, equal/reduced grids, empty/multiclass/deferred outputs, to_supervision(), all four factors, strided 300-query/192×192 masks, fallback and JIT handling.
  • Local: 151 passed, 141 CUDA skips in the four-file targeted suite; 134 adapter tests passed on current head; 266 shared postprocessing/empty-grid tests passed. An eight-case CPU boundary probe composes the real Triton assemblers and HTTP adapter for RLE/polygon × opt-in/default; this is not a GPU HTTP integration test.
  • Black/isort, critical flake8 (E9,F63,F7,F82), and git diff --check pass. Advisory flake8 retains the pre-existing boundary_active F841.
  • Existing sparse capacity and deferred overflow behavior remain unchanged. Unsupported transformations and antialiased downsampling keep their reference fallback.
PYTHONPATH=inference_models python -m pytest -q \
  inference_models/tests/unit_tests/models/rfdetr/test_triton_postprocess.py \
  inference_models/tests/unit_tests/models/rfdetr/test_triton_jit_fallback.py \
  inference_models/tests/unit_tests/models/common/roboflow/test_mask_image_dimensions.py \
  inference_models/tests/unit_tests/models/base/test_instance_segmentation.py
# Corrected coordinate fixture cases, run separately on afcae76a1:
PYTHONPATH=inference_models python -m pytest -q \
  inference_models/tests/unit_tests/models/rfdetr/test_triton_postprocess.py \
  -k fractional_boxes_match_final_grid_reference
Source pins and reproducible evidence
  • Jetson AGX Orin 64 GB (orin-agx-jp62, CUDA:0), MAXN; CUDA 12.6, Torch 2.6, Triton 3.2.
  • rfdetr-seg-nano, package 8b8da2fe824240522a39f3cde41aafae, Torch FP32 eager.
  • Profiler ccbaa275b342b76261c84e78a73f5a479877250f; task-local remote-runner configuration based on 6bc3898d.
  • Baseline runtime 2baa328bd1da42b28e69d43d33c8cb61e7a47cb5 (merged Align instance segmentation boxes with the mask grid #3156); measured candidate afcae76a1a816ca66c8ef05b31ecc6a05200795c. Current head 772da058d merges newer main; RF-DETR runtime blob is identical (92d418b65372cb147ddaa89569f949bfb81e8064). No claim that profiling ran at the later merge SHA.
  • Image roboflow/roboflow-inference-server-jetson-6.2.0:1.7.3, digest sha256:3c4b5193738433fb45b52af4e2a0441aef9d8473f4863bf8903f94ca194b770a.
  • Campaign: implementation-selection-mode=base, mask_format=rle, masks_resolution_factor=.25; base camera_640x480_batch_1_base, confidence .4, max detections 300, grid178×218; large camera_1920x1080_batch_4_high, confidence .01, max detections3000, grid328×538.
  • Latency: 20 warmups, 100 measured iterations per repetition. Memory: three fresh worker processes, first use plus four additional warmups and 20 measured iterations.

Artifacts on the selected host: /home/roboflow/remote-runner/runs/<run-id>/output/:

  • B/A: rfdetr-mask-quarter-coords-{before,after}-v2-20261009-optimization-handoff-1; each contains optimization-profile-set.json, artifacts/{base,large}/{latency,memory}.json, and artifacts/{base,large}/nsight/inference.sqlite.
  • Snapshots: corresponding campaign {base,large}-snapshot-1 runs; input SHA-256, arrays and masks all match exactly.
  • Paired benchmark: rfdetr-mask-coords-benchmark-afcae76a-20261009; longer full-grid repeat: rfdetr-mask-full-grid-repeat-afcae76a-20261009 (postprocess-benchmark.json).
  • Tests: rfdetr-mask-coords-tests-884260f9-20261009 and rfdetr-mask-coords-tests-afcae76a-20261009; run.log is beside output/.

Earlier pre-#3156 measurements are superseded by the results above.

Validation decision Base Large
Model, runtime configuration, input/load plan and base mode comparability pass pass
Candidate sparse kernels execute without reference fallback pass: CUDA trace and paired parity assertions pass: CUDA trace and paired parity assertions
Snapshot input identity pass: exact SHA-256 pass: exact SHA-256
Snapshot output parity, factor 0.25 pass: 17 detections, exact arrays/dtypes/masks pass: 400 detections, exact arrays/dtypes/masks
Warm complete-inference latency, factor 0.25, criterion ≤ baseline median and p95 pass pass
Full-grid warm postprocessing latency, factor 1.0, current vs previous Triton, strict zero increase inconclusive: repeat medians +0.23–0.33%, mixed round signs inconclusive: repeat medians +0.73–0.91%, mixed round signs
Incremental CUDA allocator peak / retained growth, factors 0, 0.25, 0.5, 1; respective controls above pass pass
Cold first-use latency and first-process RSS, factor 0.25, criterion ≤ baseline fail fail
Total process GPU memory inconclusive: NVML unavailable inconclusive: NVML unavailable
Host telemetry comparability pass for available channels; GPU clocks unavailable pass for available channels; GPU clocks unavailable
Trace explains improvement fewer launches, copies and synchronizations fewer launches, copies and synchronizations

The full-grid repeat does not establish an exact zero-regression bound; median increases are below 1% and round differences change sign. All four repeat cases match the reference exactly and retain the same CUDA allocation peaks with zero retained growth.

Overall recommendation: human decision required on the cold-start cost measured at factor 0.25. Coordinate alignment is implemented and verified, and reduced-grid warm performance improves. Keep the PR draft rather than presenting it as meeting unconditional latency/memory no-regression acceptance.

MTaraszewski and others added 30 commits October 1, 2026 15:11
…factor

mask_decode_mode and tradeoff_factor are accepted end to end and then
ignored: they pass untouched through
InferenceModelsInstanceSegmentationAdapter.map_inference_kwargs and are
swallowed by **kwargs in each model's post_process. Masks are always
produced at full input resolution.

The plan proposes a mask_resolution_factor in [0,1], resolved in the
adapter from the existing enum, carried as explicit metadata rather than
inferred from array shape. Default 1.0 is byte-identical to today.

Measured on 4K with 300 instances: polygon path 889 ms -> 92 ms, RLE path
3107 ms -> 271 ms, peak RSS 3256 MB -> 595 MB at t=0.25. On Jetson Orin NX
the stage currently moves ~40 GB per frame.

Documents the blocking prerequisite: there is no mask-to-image coordinate
scaling in the inference_models response path, so reduced-resolution masks
would silently corrupt every polygon and RLE payload.
The existing coverage of align_instance_segmentation_results compares
chunked output against monolithic output. Both sides move together when
the resize target changes, so neither test can detect a change in output
geometry. crop_masks_to_boxes had no coverage at all.

Add characterization tests that pin the current contract: masks sized to
size_after_pre_processing, bool dtype, axis order on a non-square image
(a transpose is invisible on a square one), a golden per-instance pixel
count from a fixed seed, and that the static-crop paste relocates onto a
larger canvas without changing the set-pixel count. Add first coverage
for crop_masks_to_boxes, including one test documenting that `scaling`
is a single scalar applied to both axes.

Add scale_polygons_to_image, which lifts contour coordinates from mask
space into image space, scaling each axis independently since the two
need not share an aspect ratio. The legacy path does this via
post_process_polygons / scale_polygons (inference/core/utils/postprocess.py);
the inference_models rewrite dropped it, which is harmless only while
masks happen to be image-sized. No caller is changed here.

Tests: python -m pytest tests/unit_tests/models/common/roboflow/test_post_processing.py
45 passed (31 pre-existing, 14 added).
Supersedes the earlier plan, whose headline figures combined two code
paths that never execute together and whose recommended v1 is measurably
slower than today on the default RLE path.

Records the team decisions from review: new block versions are the
delivery mechanism, mask_size as carried metadata is accepted in
principle, and the RLE wire-format decision is deferred to the end of
implementation. Adds the TDD sequence, including why the existing
chunking tests cannot detect a geometry change.

Applies black and isort to the files touched by the previous commit.
Committed in error. This branch carries Python changes only; the files
remain on disk, untracked.
Interpolate the mask resize target between the post-unpad mask grid and
size_after_pre_processing, instead of always resizing to the latter. A
factor of 1.0 is the default and short-circuits to the image dimensions,
so round() never runs on the default path and output is bit-identical.

The empty-detection branch and the static-crop canvas both follow the
factor. The canvas keeps the mask grid's ratio to the image rather than
planting a reduced mask on a full-size canvas, which would have been
correctly placed but saved no memory; crop offsets move into the same
space. Consumers that derive a canvas from mask.shape therefore get a
consistent answer with and without detections.

No caller passes the parameter yet.

Tests: python3 -m pytest tests/unit_tests/models/common/roboflow/test_post_processing.py
58 passed (45 pre-existing, 13 added).
mask.shape[1:] and InstancesRLEMasks.image_size are currently the de
facto definition of the mask canvas at roughly fifteen call sites. Once
the resize target becomes adjustable they stop agreeing with the image,
so the grid has to travel with the prediction rather than be inferred.

Add mask_size to InstancesRLEMasks and InstanceDetections, each
defaulting to the value it effectively had before: image_size for the
former, the carrier's own grid for the latter. Every existing
construction stays valid and every reader that ignores the field gets
today's behaviour.

to_coco_rle_masks still writes image_size as the COCO size field. The
wire format is a separate, deferred decision and is untouched here.

Tests: python3 -m pytest tests/unit_tests/models/base tests/unit_tests/models/common/roboflow/test_post_processing.py
77 passed.
Follows .cursor/rules as AGENTS.md requires: keyword-only secondary
arguments via `*`, a blank line after guard clauses, and named return
values instead of expressions on the return line.

No behaviour change; every call site already passed these arguments by
keyword.
Both parameters were accepted end to end and then ignored: they passed
untouched through map_inference_kwargs and were swallowed by **kwargs in
each model's post_process.

Collapse the published enum onto the single resolution factor that
post-processing consumes. accurate maps to 1.0, fast to 0.0, tradeoff
reads tradeoff_factor; an absent mode stays at 1.0, so default behaviour
is unchanged. Both source keys are consumed, since the deeper pre/post
stages do not accept arbitrary kwargs. An unknown mode now raises
InvalidMaskDecodeArgument, matching the legacy path, which previously
accepted anything here.

Scale polygon coordinates from mask space into image space where every
mask branch converges, using the mask grid now carried on the detection.
Equal sizes short-circuit to a no-op, so today's output is untouched;
without this a reduced-resolution mask would report coordinates shrunk
toward the origin. The legacy path does this via post_process_polygons;
the inference_models rewrite dropped it.

Tests: python -m pytest tests/inference/unit_tests/core/models/test_inference_models_adapters.py
37 passed (27 pre-existing, 10 added).
The dense path already took the factor but nothing passed it, so the
parameter was reachable from a request and still inert. Thread it through
all six instance-segmentation families - yolov5, yolov7, yolov8, yolo26,
yolact and rfdetr - on both the dense and RLE routes, across the ONNX,
TorchScript and TensorRT backends. Their post_process methods all accept
**kwargs, so an un-threaded parameter is silently swallowed, which is how
the original regression happened.

Teach align_instance_segmentation_results_to_rle_masks the same
interpolation the dense path uses, and scale its static-crop canvas and
offsets to match, so mask_format no longer changes the resolution a
caller receives.

to_coco_rle_masks now declares the grid the counts were encoded on rather
than the image size. Measured: pycocotools decodes an honest reduced size
cleanly, decodes a mismatched one silently wrong, and
sv.Detections.from_inference auto-resizes an honest one to the image. The
previous behaviour - declaring image_size while encoding at
size_after_pre_processing - is the mismatched case as soon as the two
differ, so declaring the encoded grid is the correction, not the risk.

RF-DETR's fused Triton post-process interpolates to the image inside the
kernel and cannot honour a reduced target. It now reports
mask_resolution_factor_unsupported and defers to the eager path instead
of silently returning full-resolution masks. Passing the reduced output
size into the kernel is the better answer and is left as a follow-up; it
already parameterises its resize tables by output size.

Default remains 1.0 and short-circuits before any arithmetic, so output
is unchanged.

Tests: python3 -m pytest tests/unit_tests -q
1597 passed. The 12 failures are pre-existing and environmental: five
compare realpath-resolved against unresolved temp paths and cannot pass
on macOS, seven need the optional sam3 package.
The first draft's headline figures combined two code paths that never
execute together, and its recommended v1 was measurably slower than the
status quo on the default RLE path. Both are corrected.

Restructures to the repository implementation-plan template: one
recommended solution, Before/After flows on the same 4K example, and
Question/Investigation/Recommendation/Input for each open item. Records
the team's review decisions - new block versions as the delivery
mechanism, mask_size as carried metadata, RLE wire format settled at the
end of implementation - and folds in the measured evidence that settled
the wire question.

Adds a sequencing section covering what is built, what remains and why
the Triton gate belongs with the RLE work rather than before it. The TDD
sequence is now an appendix rather than a separate document, so the plan
and the test strategy stay in one file.
mask_decode_mode and tradeoff_factor are declared on every version of the
block, and were forwarded to the server, which discarded them. Now that
the server honours them, those versions would silently change the masks
an existing workflow produces - a v3 step set to "fast" would start
returning model-resolution masks on upgrade.

Versions up to v4 therefore pin the outbound request to accurate/1.0,
preserving the behaviour they have always had. Their manifests keep both
fields, so existing workflow JSON still validates, but the fields are now
deliberately inert rather than accidentally so. @v5 is the version where
setting them does what their description says.

v5.py and v5_tensor.py differ from their v4 counterparts in four places:
the manifest version, the type literal, the class name, and the two
outbound calls. The versioning mechanism is file copy rather than
inheritance, so the two siblings share a class name and are selected by
ENABLE_TENSOR_DATA_REPRESENTATION at import time.

Tests cover the split - legacy versions pin, v5 forwards - and assert
manifest parity between each version and its tensor-native sibling, which
is held by convention today and nothing else.

Also fixes three rfdetr backends where masks_resolution_factor was
forwarded from post_process but declared on from_pretrained. flake8 F821
caught it; the TensorRT paths are not exercised by CPU tests.

Tests: python3 -m pytest tests/unit_tests -q (inference_models) 1942 passed
       python3 -m pytest tests/workflows/unit_tests -q 1265 passed
       uvx flake8 --select=E9,F63,F7,F82 clean
The plan called reducing max_detections a cheaper, lossless first move.
Measured, it is neither: halving it saves 55% where the resolution factor
saves 90%, and capping at 150 in a scene with 300 objects deletes 150 real
detections rather than degrading them. Replaces the claim with the data.
The plan called reducing max_detections a cheaper, lossless first move.
Measured, it is neither: halving it saves 55% where the resolution factor
saves 90%, and capping at 150 in a scene with 300 objects deletes 150 real
detections rather than degrading them.

Adds the fidelity curve against t, by object size, and states plainly what
it is not. Aggregate mask IoU reads 0.908 at t=0.25 while boundary IoU is
0.654 and small objects are at 0.754, so evaluating this change on
aggregate IoU alone reaches the wrong conclusion. Small objects pay roughly
five times what large ones do, which makes a single recommended default
workload-dependent.

True AP_mask against ground truth cannot be produced in this workspace -
there is no labelled set and no model weights - so the feature ships with
a measured fidelity curve and no recommended value.
Phase 6 is split into what landed and what did not. Steps 1, 2 and 4 are
done; the Triton kernel is gated rather than taught, and remote version
skew is unhandled.

Documents both deferrals with their impact. The Triton one is not free:
the fused CUDA path is the fast path, so gating means t<1 falls back to
the slower eager generator, and whether reduced-resolution-eager beats
full-resolution-fused is unmeasured. Enabling the knob could make
RF-DETR-seg slower on GPU. That needs one CUDA measurement before the
knob is documented for GPU users.

Narrows the version-skew item. run_locally uses the in-process model
manager, so the block and the model layer are the same installation and
skew cannot occur locally; only run_remotely can reach an older backend.
The execution-engine compatibility guard does not cover this, since it
gates engine schema semantics rather than post-processing behaviour.

Drops the Author field, which git already records.
Four blockers and three majors from review, each reproduced before the fix
and verified after.

RLE-backed polygons were decoded on image_size while the counts describe
the encoded grid. The adapter requests RLE whenever the model supports it,
so this was the default polygon route and returned unusable contours.
Both decoders now use the encoded grid.

Static crop with a reduced factor could crash: the canvas size and the crop
offset were rounded independently, leaving the canvas a pixel short of the
pasted extent. The canvas now contains that extent. Only crops reaching the
image edge under letterbox scaling expose it, which is why a sweep with zero
padding found nothing.

A static crop anchored at (0, 0) yields a mask smaller than the image whose
coordinates are already image-space. Inferring "reduced grid" from
mask_size != image_size scaled those a second time at the default factor.
Scaling is now relative to size_after_pre_processing.

The tensor-native local call sites were unpinned in v1-v4. All four are
pinned. v5_tensor is pinned too: nothing under workflows/ reads mask_size
and the sites that rebuild InstancesRLEMasks drop it, so reduced resolution
is available in non-tensor mode only.

The pin comment claimed these fields never took effect. That is false on the
legacy ORT backend, which honours all three modes and is the default on
Windows. Corrected in all twelve places and recorded in the changelog as a
deliberate behaviour change there.

The Triton dispatcher dropped the factor, so its gate was unreachable.
to_supervision returned a reduced mask beside image-space boxes, and
__iter__ declared the image size for counts on the encoded grid.

Zero-detection results reported a shape the populated path would never
produce. Both changelogs added. Plan document removed from the published
docs tree. Formatter-only churn in three unrelated files reverted.

Tests rewritten around the review's central point, that the first set could
not fail: 24 golden cases captured from origin/main with non-zero padding
throughout, dense-versus-RLE content comparison, the static-crop crash
combination, RLE-backed polygons at reduced resolution, and a dispatcher
spy rather than a direct call to the gate.

Tests: inference_models 2016 passed, workflows 1274 passed,
inference core/models 121 passed, flake8 clean.
Review raised that "fast" and "accurate" mislead when the image is smaller
than the model's mask grid. resolve_mask_target_size interpolates between
the mask grid at 0.0 and size_after_pre_processing at 1.0, so when the
image is the smaller of the two a lower factor produces MORE mask pixels,
not fewer: with a 160x160 grid and a 100x100 image, 0.0 yields 25,600
pixels against 10,000 at 1.0.

States the endpoints explicitly in the function docstring and in the
tradeoff_factor field description on both @v5 files, including that a
lower factor does not guarantee smaller masks or lower latency, and that
callers sensitive to this should compare size_after_pre_processing against
the mask grid rather than assume a direction.

Documentation only; no behaviour change.
Resolves three conflicts, all additive on both sides: an import block in
the adapter tests, and Unreleased entries in both changelogs.

Review raised that pinning v1-v4 unconditionally removes behaviour that
currently works. The legacy backend honours mask_decode_mode and
tradeoff_factor; only inference_models discarded them. Pinning everywhere
traded one group's breakage for another's.

The pin is now conditional on USE_INFERENCE_MODELS, so those versions keep
passing the fields through on the legacy backend and behave exactly as they
do today. No existing workflow changes behaviour on either backend.

workflows/ cannot import inference.* - the isolation probe forbids it - so
the flag travels as ModelsConfiguration.use_inference_models, bound in
roboflow_workflows.environment and populated by the host from
inference.core.env, matching how every other such value reaches the package.

The tensor-native siblings stay pinned unconditionally. Their reason is
downstream rather than the backend: nothing under roboflow_workflows/ reads
mask_size and the sites rebuilding InstancesRLEMasks drop it, so a reduced
grid is reinterpreted as image-sized whichever backend produced it.

Tests: inference_models 2242 passed, workflows 1274 passed,
inference core/models 129 passed. Twelve inference_models failures are
pre-existing and environmental (triton is Linux-only; five path tests
compare realpath-resolved against unresolved and cannot pass on macOS).
Completes the conditional pinning across the non-tensor block versions:
v1-v4 now send accurate/1.0 only when USE_INFERENCE_MODELS is set, and pass
the caller's values through otherwise. The legacy backend honours these
fields, so those versions behave on it exactly as they do today.

The tensor-native siblings stay pinned unconditionally. Their reason is
downstream rather than the backend: nothing under roboflow_workflows/ reads
mask_size and the sites rebuilding InstancesRLEMasks drop it, so a reduced
grid is reinterpreted as image-sized whichever backend produced it. The
comments now state which reason applies where.

Documents ModelsConfiguration.use_inference_models in the changelog. It is a
public field and a host serving through the legacy backend must set it to
False, or v1-v4 stop forwarding fields that backend honours - reintroducing
the regression this change exists to avoid.

Applies black and isort. Eight files needed reformatting, all of them
introduced by this branch; the equivalents on origin/main were already clean.

Tests: inference_models 2242 passed, workflows 1274 passed,
inference core/models 129 passed. black, isort and flake8 clean across all
47 changed Python files.
The release cut inference_models 0.39.1 and roboflow-workflows 0.2.4. Merging
it dropped this branch's inference_models entries and moved its workflows entry
into 0.2.4, attributing it to a release that does not contain it.

Both merged without conflict, so neither was flagged.
…ce_segmentation/v3_tensor.py

Co-authored-by: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com>
Co-authored-by: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com>
Place origin crops on the original-image canvas for dense and RLE outputs. Preserve tensors when encoding dense adapter results as RLE. Add real-model integration, HTTP server, and platform workflow regression coverage.

Validated in the existing Python 3.12 .venv: 406 regression tests passed, 3 skipped; 6 ONNX and 3 Torch integration cases passed; 12 HTTP server cases passed; 6 local workflow checks passed using a public model. Black, isort, and git diff --check passed. TensorRT execution and authenticated staging E2E remain pending.
Base automatically changed from codex/pr-3113-ci-fixes to main October 9, 2026 13:18
…ask-tradeoff

# Conflicts:
#	docs/instance_segmentation_mask_resolution.md
#	inference_models/docs/changelog.md
#	inference_models/tests/unit_tests/models/rfdetr/test_triton_postprocess.py
#	workflows/CHANGELOG.md
#	workflows/roboflow_workflows/core_steps/common/inference_response_entities.py
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for the pull request! Here is how automated Claude review works here, so you spend credits (and reviewer time) wisely.

🚦 This PR is marked Ready for review, so automated Claude review will run — and every pass spends real credits.

Warning

💸 The Claude reviewer bills in credits, not vibes

Automated review spins up a real agent that reads real code and spends real credits on every pass. It is glad to help — but it is not a rubber duck, a linter you poke in a loop, or a substitute for reading the contributing guide. Treat it like an expensive senior reviewer whose time you booked, and show up prepared.

Draft when unsure, Ready when you mean it:

  • 🌱 Not sure the PR is in good shape yet? Keep it (or set it back) as a draft — drafts pause review, so you can push and iterate without burning credits on a moving target.
  • 💪 Feel strong about the contents? Mark it Ready for review and the reviewer will take a look.

However you get there, arrive prepared:

  • 🧱 Bring a SOLID, thorough PR. Point your local agent at our skills/ to tune it to our guidelines first — or, if you are one of those fabled carbon-based contributors, read them yourself. A half-baked diff costs exactly the same to review as a finished one.
  • ✅ Resolve every comment before you re-request review. Re-requesting with threads still open means paying twice for the same conversation.
  • 🔁 Do not use CI review as an inner loop for a local agent. The reviewer is not a step-by-step debugger — do the unfolding locally and arrive with the answer, not the search.
  • 🙋 If something looks off, ask a human. One question to a maintainer is cheaper and faster than three rounds of agent re-review chasing a misread.

Reviews are not free. A draft costs nothing to review; a Ready PR is a promise that it is worth reviewing.

  • Prefer to skip automated review entirely? Add the skip-claude-review label.

@dkosowski87 dkosowski87 added the claude-review Use to trigger AI review manually label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude review started at commit a52a201a1cfc4a5b4c9753b4a41eb8795925b90b.

New commits are not auto-reviewed. Add the claude-review label to request a re-review — the label is consumed when the review starts, so just add it again next time.

@github-actions github-actions Bot removed the claude-review Use to trigger AI review manually label Oct 9, 2026
"cuda_device_required" if factor == 0 else "mask_downsampling_unsupported"
)


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: none of the new CUDA parity tests run in CI.

These tests are the only coverage for the rewritten kernels: _box_coordinate_on_mask_grid, _selected_query_class_metadata_kernel, the per-query retry flag and the reduced-grid RLE path. They are gated only by skipif(not torch.cuda.is_available() ...) and carry no gpu_only / trt_extras marker. Nothing in CI collects them:

  • unit_tests_inference_experimental.yml runs -m "not gpu_only" on CPU runners, so every one of them is skipped there.
  • unit_tests_inference_experimental_gpu.yml runs -m "gpu_only and trt_extras", so it deselects them.

This applies to test_triton_mask_grid_matches_reference_on_cuda, test_triton_native_and_large_source_grids, test_triton_global_topk_keeps_more_than_four_classes_per_query, test_ignored_classes_on_inactive_query_consume_global_topk_slots, test_triton_2xlarge_strided_queries_match_reference and test_triton_fractional_boxes_match_final_grid_reference. A later regression in box arithmetic, global top-k or mask-grid parity would merge with CI green. The Orin runs in the PR description are manual and can't be repeated by CI.

Fix: add @pytest.mark.gpu_only and @pytest.mark.trt_extras to each CUDA-gated test, as test_triton_object_detection_postprocess.py and test_triton_universal_preprocess_runtime.py already do. A module-level pytestmark won't work here because the file also has CPU tests. Please also mark this file's existing CUDA-gated tests while you are in it.

(Skill: review-topic-test-hygiene, Standard 4)

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@dkosowski87 — please answer the question below. @PawelPeczek-Roboflow, you approved this PR, so your input is needed too.

⏳ This PR is on hold pending your answers. The review will not advance to sign-off until the IMPORTANT question below is answered.

IMPORTANT questions

  1. Is the cold-start regression accepted, and is the "remains a draft" wording out of date? The PR description says this change "does not satisfy unconditional no-regression acceptance", that it should be "kept draft", and that it needs a "human decision required on the cold-start cost". The measured cost at factor 0.25 is first use going from 1.44 s to 10.11 s (base) and from 1.74 s to 10.98 s (large), with a higher first-process RSS. However, the PR is marked ready and already approved, and neither the PR nor its comments record that decision.
    • The cost only applies when INFERENCE_MODELS_RFDETR_TRITON_POSTPROC_ENABLED is on (default False) and the factor is not 1.0. Before this PR, that combination used the reference path and compiled nothing.
    • The new kernels take box_scale_width / box_scale_height / reduced_grid as tl.constexpr (triton_postprocess.py, _select_best_query_metadata_kernel and _selected_query_class_metadata_kernel). Every distinct image size × factor therefore triggers its own compile, so mixed-resolution streams pay the cost more than once.
    • What would change the outcome: if a maintainer has not explicitly accepted this cost for Triton-enabled deployments (or asks for mitigation, such as passing the scales as runtime args), the PR is not merge-ready.

Unanswered questions may keep this PR out of a release.

New commits are not auto-reviewed. When you have answered and addressed the inline finding, add the claude-review label to request a re-review.

Reviewed at HEAD: a52a201

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Claude review summary: not merge-ready. There is 1 blocking finding (inline) and 1 open IMPORTANT question (in the comment above).

Skills: review-inference-models-pkg, review-topic-prediction-integrity, review-topic-test-hygiene

No dedicated surface skill covers docs/instance_segmentation_mask_resolution.md, so that file got the generic review plus topic skills only.

Blocking

  • High: none of the new CUDA parity tests in test_triton_postprocess.py carry gpu_only + trt_extras. The CPU workflow skips them and the GPU unit workflow deselects them, so no CI job checks the rewritten kernels. Details are in the inline comment.

Checked against the reference (rfdetr/common.py and common/roboflow/post_processing.py), no issue found

  • Boxes: the mapping from normalized coordinates through the network size to the mask grid matches _align_boxes_to_mask_grid. That covers the reduced-grid scale *= image/target, float32 promotion on reduced grids, dtype rounding on the image grid, clamping, and finalize_instance_segmentation_boxes.
  • Selection: global top-k over num_queries flattened pairs matches select_topk_predictions. Clamping max_detections to num_queries is equivalent to the reference cap.
  • Retry: the first-pass retry now also fires when an ignored class passes the threshold, which matches the reference's top-k slot consumption.
  • Query rows: the query metadata rows only set fields 0 and 9. That is sufficient, because the source-bounds and RLE kernels read only those fields.
  • Fallback: the downsampling guard (target < mask grid) routes antialiased resizes to the reference path, and padding, static_crop and resize metadata still fall back as before.
  • Interpolation tables: the vectorized build in _get_interpolation_weights keeps row-major tap ordering.
  • Unrelated pass-through: removing .contiguous() on masks is safe, because both mask kernels address through explicit strides.
  • Retry flag: the per-row retry flag removes the earlier race on records[0,1].

Tests to add: none beyond routing the existing CUDA tests into the GPU job (see the inline finding).

Commands that informed this review: gh pr view/diff 3154, the paginated gh api comments/reviews/commits listings, grep/sed over review-source/inference_models/** and .github/workflows/unit_tests_inference_experimental{,_gpu}.yml, and inference_models/pytest.ini.

Reviewed at HEAD: a52a201

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Release coordination

  • Contributor: no action needed. The user-facing entry is already under ## Unreleased in inference_models/docs/changelog.md.
  • Maintainers: inference-models needs a version change at release time, because this changes the behavior of the RF-DETR Triton post-processor at reduced mask-resolution factors. This does not affect roboflow-workflows or the Execution Engine.

Reviewed at HEAD: a52a201

@dkosowski87

Copy link
Copy Markdown
Collaborator Author

The measured cost at factor 0.25 is first use going from 1.44 s to 10.11 s (base) and from 1.74 s to 10.98 s (large), with a higher first-process RSS. However, the PR is marked ready and already approved, and neither the PR nor its comments record that decision.

The cost of around 10s latency on the first run is the Triton kernel compilation. The 1.44 on the baseline is because Triton was not used and there was a fallaback. So this PR didn't introduce this.

The memory regression is reasonable for the performance improvement and unblocking this path.

@dkosowski87
dkosowski87 merged commit ef347fb into main Oct 9, 2026
88 of 89 checks passed
@dkosowski87
dkosowski87 deleted the codex/rfdetr-triton-mask-tradeoff branch October 9, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants