Skip to main content

RCNN proposals and level assignment

Params::profile = Profile::RcnnProposalsV1 implements an explicit proposal recipe, not a complete two-stage detector or ROI execution service. Use Task::Detection, one class, five ordered heads and the common typed check_inference → Plan → ExecutionContext → execute lifecycle.

Inputs and outputs​

The ordered feature sizes are 136,68,34,17,9, each square. Each head has:

  • one coordinate tensor with 12 channels: three anchor groups of x/y/width/height deltas;
  • one score tensor with three channels: one objectness logit per anchor;
  • matching INT8 or BF16 dtype and H/W between those two tensors.

Every INT8 port needs its own explicit quantization entry. Sigmoid is the fixed score activation; it may be omitted from parameters or supplied consistently. Dense/padded and admitted MlaCBlocked readers share the same logical recipe. Physical padding never supplies a fourth anchor.

There is no auxiliary input. The fifth output is contiguous INT32 [K], containing the derived level 0..3 for each accepted proposal. The other outputs are boxes [K,4], scores [K], class IDs [K] and count [1]; class IDs are zero. By default (rcnn_per_level_topk=0), max_detections is global final capacity, not a per-level or pre-NMS limit. Boxes use INT32 rounded XYWH. Results are score-ordered, not grouped by level. A downstream ROI integration must regroup the valid prefix if its consumer requires level-grouped proposals.

Optional per-level proposal selection​

Set rcnn_per_level_topk=P (1..16384) and max_detections=4*P when the consumer needs up to P proposals from each of the four levels. The coordinate output must then be FP32 XYWH, retaining fractional original-image geometry; other output dtypes and roles are unchanged. This option is RCNN-only.

Selection remains global score-ordered NMS. A survivor beyond its level's output quota still suppresses later candidates in other levels. Only after suppression are the first P survivors of each level retained, in global score order. Level assignment uses model-space area before original-image scaling and clipping. Execution stops once all four quotas are full, since no later score can enter the result. Context creation reserves candidate-bound NMS rectangles for this opt-in, but retains full proposal geometry only up to the output capacity 4P. Quota-discarded survivors therefore participate in suppression without occupying the proposal vector or requiring a compaction pass. Execution does not allocate. The default global path retains its smaller final-capacity NMS scratch.

The kernel does not serialize a framework wire format. A consumer may group the valid prefix by the supplied level index and encode coordinates as its format requires. For example, Core's RCNN stage-one serializer emits four groups of int32 count followed by P records of six FP16 values: score, reserved_zero, x1, y1, x1+width, y1+height. Conversion happens after original-image clipping, not before NMS or level assignment.

Prepare the anchor geometry once​

For head index h, let s = 1 << h. The cell-to-model-pixel strides are:

Head indexFeature sizesCell stride
013614
16828
234416
317832
491660

The final stride is explicitly 60, not 64. This fixed recipe is not inferred from the model image dimensions or replaced by a generic feature-pyramid rule.

Preparation evaluates root = std::sqrt(2.0f) into a float. The three base widths are 16.0f*root, 16.0f, 16.0f/root; base heights use the reverse order. For each base component b, its anchor dimension is calculated in this order:

  1. Multiply the float base by head factor s.
  2. Apply the float overload of std::round to that product.
  3. Multiply the rounded result by two and retain the float anchor dimension.

In source notation, this is std::round(b * s) * 2, not std::round((b * s) * 2). At head zero, the first anchor's width becomes round(16*sqrt(2))*2 = 46; moving the doubling inside the rounding would give 45 instead. The prepared head-zero width/height pairs are (46,22), (32,32), (22,46).

Rounding is part of this recipe's geometry, not an approximation added by the new interface. Precomputing it removes repeated square roots and anchor rounding from each candidate decode without changing their order. The authoritative code is prepare_rcnn_geometry (A65/boxdecode/detail/geometry.h).

Decode deltas only when needed​

The prepared coordinate reader locates the selected anchor's four channels. It expands BF16 bits or dequantizes INT8 using that coordinate port's scale and zero point. The anchor center is (cell_x * stride, cell_y * stride): no half-cell shift is added.

A separate dtype-specialized RCNN decoder evaluates these source expressions:

ComponentOrdered expression
Widthanchor_width * std::exp(expanded_width_delta)
X centeranchor_x + (expanded_x_delta * anchor_width)
Heightanchor_height * std::exp(expanded_height_delta)
Y centeranchor_y + (expanded_y_delta * anchor_height)

There is no SSD 0.1/0.2 coder factor in this recipe. Do not replace an exponential with sigmoid, reinterpret widths as already-decoded extents, or fold the anchor into a different expression and assume identical floating-point behavior. The current path uses explicit float math overloads for the exponential; this page does not impose a new blanket no-FMA or all-binary32-intermediate contract on the entire BoxDecode implementation.

For INT8 coordinates, preparation evaluates that same float exponential for all 256 raw payloads using the coordinate port's scale and zero point. Execution looks up the result and then performs the anchor multiplication in its original order. For BF16 width/height deltas, the two exact signed-zero encodings use exp(+0) = exp(-0) = 1.0f. Every other encoding, including the smallest subnormals, infinities and NaNs, retains the direct float exponential. This is an exact identity, not a small-value approximation. The separate RCNN helper keeps unrelated family geometry out of its decode path; score admission, arithmetic grouping and lazy geometry validation do not change.

The immutable INT8 table is shared with the plan, not cached from frame data; independent contexts do not mutate it. Overflow and underflow results remain represented in the table, and the normal geometry-validity checks still decide whether to retain the box. This changes where work runs, not the allowed input values or decoding formula.

Score access paths​

The score scan preserves logical cell/anchor order and emits finite activated scores meeting the threshold. Current implementation choices are:

  • INT8: use the bounded three-channel reader. A maximum-of-three raw-value check can reject an entire background cell before evaluating its individual anchor scores. Prepared INT8 activation tables avoid repeated sigmoid work. The statically INT8 branch reads its prepared table directly, avoiding a redundant runtime dtype check for each anchor; finite checks, thresholds and candidate ordering remain unchanged.
  • Dense BF16: walk the three logical scalar channels in each physical pixel directly. The physical pixel stride may include padding. Select an exact raw-interval predicate once per head, then evaluate the same float sigmoid only for anchors that pass it. No whole-cell prepass is added.
  • Packed BF16 with a complete three-channel slice: use the separate scan_rcnn_bf16_packed helper to walk physical tile runs in logical row order. A conservative high-byte test can reject all three anchors together. Otherwise, each anchor retains its individual high-byte check before full BF16 reconstruction. Final admission still checks the complete key and score.
  • Packed BF16 with split channel slices: use the bounded prepared reader; valid small/odd BF16 channel tiles remain supported rather than being rejected to simplify the optimized scan.

Dense raw intervals​

For the prepared full-key cutoff cutoff, dense BF16 admission uses:

  • When cutoff <= 0x8000, accept unsigned(raw) < 0x10000u - cutoff.
  • Otherwise, let base = cutoff ^ 0x8000u; accept uint16_t(unsigned(raw) - base) < 0x8000u - base.

These are exact rearrangements of key(raw) >= cutoff, where negative words have key uint16_t(~raw) and nonnegative words have key raw ^ 0x8000. The bound remains unsigned so 65536 is representable. The intervals include all raw encodings, including signed zeros and NaN payloads; subsequent sigmoid and finite-score checks still determine whether to emit a proposal.

Preparation fixes RCNN activation to sigmoid and dispatch selects this helper only for BF16 heads. The helper therefore omits redundant runtime dtype, activation and cutoff checks after interval admission. It does not change the sigmoid expression, score threshold, K1/multi-candidate policy or anchor order. See bf16_cutoff.h (A65/boxdecode/detail/bf16_cutoff.h) and the raw/cutoff boundary tests (A65/boxdecode/tests/rcnn_interval_test.inc).

Packed high-byte bounds​

For a prepared full-key cutoff cutoff, let cutoff_high = cutoff >> 8 and first_rejected_high = max(0x81u, 0x100u - cutoff_high). A negative BF16 word has sortable key ~raw, limited to 16 bits. When its raw high byte is at least first_rejected_high, the key's high byte is strictly below cutoff_high, regardless of the low byte. The whole-cell test rejects only when all three real anchor high bytes meet this bound. It checks anchors in order and stops at the first byte below the bound.

The bound uses an unsigned value: 256 disables whole-cell rejection when cutoff_high is zero. The raw high-byte bucket 0x80 always reaches the per-anchor checks; it includes negative zero and negative subnormals. Negative zero's full sortable key is 0x7fff, not positive zero's 0x8000.

Preparation derives the cutoff from the actual float sigmoid results at BF16 values. The byte test does not approximate a score, replace sigmoid or change the threshold: small negative logits whose rounded sigmoid meets the threshold remain eligible. Only the three real channels participate; poisoned padding must not affect admission. Anchor geometry, finite-score checks, selection and tie order are unchanged. See rcnn_scan.h (A65/boxdecode/detail/rcnn_scan.h).

Selection, original coordinates and level output​

Use the common descending-score order and deterministic head/cell/anchor tie order. Geometry is validated before admission; greedy class-aware NMS uses the integer rectangle/IoU policy. Because this recipe has one class, every admitted proposal participates in that class's suppression. Invalid or suppressed high scores must not prevent a lower valid proposal from filling available capacity.

After selection, centered-letterbox inversion and clipping produce the common original-image boxes (rounded INT32 by default, FP32 for per-level selection). The level calculation is separate:

  1. Preserve the selected box's model-space area, already computed as std::lround(model_width * model_height) for NMS. It is not the area of the clipped original-image output or of the rounded integer-corner rectangle.
  2. The shared geometry check has already rejected model area below 0.5, so the rounded integer area is positive. Evaluate std::log2(std::sqrt(area) / 224.0) and store it in a float sq_log. The integer argument to std::sqrt and the 224.0 literal select double arithmetic before that explicit float storage boundary.
  3. Compute floor(sq_log + 4.0 + 1e-6), convert to integer, clamp to [2,5], then subtract two to publish 0..3.

The float storage boundary and 1e-6 offset are part of the current source sequence. A simplified log-area expression or a level computed after resizing can change boundary assignments. The fixed formula is in BoundingBox::level_map (A65/boxdecode/detail/numeric.h); publication is in outputs.h (A65/boxdecode/detail/outputs.h).

Only the count-defined output prefix contains proposals. All unused common and level-output slots are zeroed on successful reuse. Kernel execution does not schedule the second RCNN stage, allocate ROI buffers or create a worker pool. See the common memory contract and correctness suite (A65/boxdecode/tests/correctness.cpp) for validation.