Batching Matching

This page documents the batched matching (HungarianAssigner2DBatched) used by FocalHeadBatched.

You can find the implementation in the repository here:
  • HungarianAssigner2DBatched: demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py

  • HungarianAssigner2D (non-batched reference): demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d.py

  • FocalHeadBatched: demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py

  • FocalHead (reference): demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head.py

Overview and Goals

The batched Hungarian 2D matcher (HungarianAssigner2DBatched) is used in the focal head with batched loss computation (FocalHeadBatched).

The matcher is designed to enable batched processing of parts of the matching process. Most notably, the cost matrix computation is performed in a batched manner, while the actual Hungarian matching is performed on a per-sample basis on the CPU (as the used implementation of the Hungarian matching algorithm requires this).

The implementation is very similar to the matcher implementation in the batched-processing example (see Example and the comments in matcher.py in the implementation).

For implementation details, see notes in the assign() method of this class.

Pre-processing

In contrast to the original implementation, the matching is performed for the whole batch at once. RaggedBatch objects are used to handle the input data.

The following code snippet shows the pre-processing step, which is the first step performed inside the assign method (which performs the matching). Please refer to the highlighted comments (marked with #@NOTE) for more details.

(As a side note, we use NVTX ranges to annotate major phases for profiling, using the NVTXRangeWrapper class.)

demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py
146        # ===== Pre-processing =====
147
148        
149        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign")
150        assert gt_bboxes_ignore is None, \
151            'Only case when gt_bboxes_ignore is None is supported.'
152
153        #@NOTE
154        #   If multiple batch dimensions are used, we flatten them into a single dimension.
155        # This is done as
156        #   - We do not care about the batch shape, so we can flatten it.
157        #   - The cost functions used to compute the cost matrices may expect a single batch dimension.
158        # After the matching is done, we restore the original batch shape for the results.
159        # Please also see the comments at the end of the function for more details on the restoration.
160        batch_shape_orig = gt_bboxes.batch_shape
161        num_batch_dims_orig = len(batch_shape_orig)
162
163        max_num_gts = gt_bboxes.max_sample_size
164        num_bboxes = bbox_pred.size(num_batch_dims_orig)
165        # Handle empty inputs & return without further processing.
166        if max_num_gts == 0 or num_bboxes == 0:
167            matched_indices = bhl.RaggedBatch.Empty(
168                num_dims=num_batch_dims_orig + 1,
169                non_uniform_dim=num_batch_dims_orig,
170                device=bbox_pred.device,
171                batch_shape=batch_shape_orig,
172                dtype=torch.int64,
173            )
174            nvtx_wrp.range_pop("HungarianAssigner2DBatched.assign")
175            return matched_indices, matched_indices
176
177        needs_reshape = len(batch_shape_orig) > 1
178        if needs_reshape:
179            #@NOTE: Flatten the batch dimensions.
180            gt_bboxes = gt_bboxes.flatten_batch_dims()
181            gt_labels = gt_labels.flatten_batch_dims()
182            centers2d = centers2d.flatten_batch_dims()
183            #@NOTE
184            # Flatten the batch dimensions for the tensor inputs which
185            #   - Are expected to have the same batch shape as the `RaggedBatch` instances.
186            #   - Do not have built-in helper functions for this.
187            bbox_pred = self._flatten_batch_dims_for_tensor(bbox_pred, num_batch_dims_orig)
188            cls_pred = self._flatten_batch_dims_for_tensor(cls_pred, num_batch_dims_orig)
189            pred_centers2d = self._flatten_batch_dims_for_tensor(pred_centers2d, num_batch_dims_orig)
190            
191        batch_size, max_num_gts, num_bboxes = gt_bboxes.size(0), gt_bboxes.size(1), bbox_pred.size(1)
192
193        img_h, img_w, _ = img_meta[0]['pad_shape'][0]
194        factor = gt_bboxes.tensor.new_tensor([img_w, img_h, img_w,
195                                       img_h]).view(1, 1, -1)
196

Cost Matrix Computation

The cost matrices are obtained by applying multiple cost functions to the input data. For each cost function, a matrix is computed which contains the cost for each possible prediction-GT pair. In the original implementation, the cost matrices are computed for a single sample at a time. Here, we show the code for the computation of the cost matrices in a batched manner. Note that the size of the cost matrices depends on the number of GTs, and so varies between samples.

demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py
197        # ===== Compute the cost matrices =====
198
199        #@NOTE
200        #   Overall, the cost matrix computation implementation is very similar to the original implementation.
201        # Note that we expect the individual cost functions to be able to handle batched data. However, they
202        # do not need to handle non-uniform sample sizes and may assume a uniform batch size. This means that 
203        # costs functions can be re-used as is if they support basic batching.
204        # The output of the cost functions is a tensor of shape `[batch_size, num_preds, max_num_gts]`,
205        # i.e. the cost matrices are padded to the same size (`max_num_gts`) for a given batch.
206        #
207        #   The handling of the non-uniform sample sizes is done after the cost matrix is computed by using
208        # `gt_bboxes.create_with_sample_sizes_like_self(cost, 2)`. See comments below for more details).
209
210        #@NOTE: Compute the cost matrices in a batched manner.
211        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:cost matrices")
212        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:cls_cost")
213        cls_cost = self.cls_cost(cls_pred, gt_labels.tensor)
214        nvtx_wrp.range_pop()
215        # regression L1 cost
216        #@NOTE 
217        #   The `apply` method is a convenience method to apply a function to the underlying `tensor` 
218        # data of the `RaggedBatch`. Note that while here, only the data tensor (i.e. `tensor`) is 
219        # input to the lambda function, the passed function (lambda in this case) can accept more inputs. 
220        # If more inputs are defined, the function will also receive the `sample_sizes` and `mask` 
221        # of the `RaggedBatch`.
222        normalized_gt_bboxes = gt_bboxes.apply(lambda bboxs: bboxs / factor)
223        
224        #@NOTE
225        #  Here, the `normalized_gt_bboxes.tensor` is used as input to the cost function. The function expects
226        #  tensor inputs and is not implmentented with the `RaggedBatch` format in mind.
227        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:reg_cost")
228        reg_cost = self.reg_cost(bbox_pred, normalized_gt_bboxes.tensor)
229        nvtx_wrp.range_pop()
230
231        # regression iou cost, defaultly giou is used in official DETR.
232        bboxes = bbox_cxcywh_to_xyxy(bbox_pred) * factor
233
234        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:iou_cost")
235        iou_cost = self.iou_cost(bboxes, gt_bboxes.tensor)
236        nvtx_wrp.range_pop()
237
238        # center2d L1 cost
239        #@NOTE: The `apply` method is used again here.
240        normalized_centers2d = centers2d.apply(lambda ctr2d: ctr2d / factor[..., 0:2])
241
242        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:centers2d_cost")
243        centers2d_cost = self.centers2d_cost(pred_centers2d, normalized_centers2d.tensor)
244        nvtx_wrp.range_pop()
245
246        # weighted sum of above four costs
247        cost = cls_cost + reg_cost + iou_cost + centers2d_cost
248        cost = torch.nan_to_num(cost, nan=100.0, posinf=100.0, neginf=-100.0)
249        
250        #@NOTE
251        #   During cost matrix computation, we assume uniform batch size (and use filler values). However, the
252        # valid cost matrices are non-uniform in size. Along the GT object dimension, the sample sizes 
253        # correspond to the sample sizes of the GT inputs. However, the dimension iterating over the GT 
254        # objects (i.e. the `non_uniform_dim`) is `dim==2` for the matrices (`dim==1` corresponds to the 
255        # predictions). Thererefore, we create a RaggedBatch containing the matrices, with the non-uniform 
256        # dimension set to `dim==2` (instead of `dim==1` in `gt_bboxes`).
257        cost = gt_bboxes.create_with_sample_sizes_like_self(cost, non_uniform_dim=2)
258
259        nvtx_wrp.range_pop()
260

Perform the Matching

While the cost matrices are computed in a batched manner, the matching is performed one sample at a time. This is done because the matching is implemented on the CPU and does not support batched operation.

As the rest of the function is batched, we need to iterate over the samples and perform the matching for each sample individually. After that, the results are combined into a single RaggedBatch object.

Note that this is equivalent to the matching used in the original implementation. However, as there, the assign method processes one sample at a time, no loop is used there.

demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py
261        # ===== Perform the matching =====
262
263        nvtx_wrp.range_push("HungarianAssigner2DBatched.assign:matching")
264        cost = cost.detach()
265        if linear_sum_assignment is None:
266            raise ImportError('Please run "pip install scipy" '
267                              'to install scipy first.')
268        
269        #@NOTE 
270        #   Move the cost matrices to the CPU. This is needed to run the Hungarian
271        # matching on the CPU. Note that this is performed before splitting the cost matrices into individual
272        # samples, so that data for all samples is moved in one go.
273        cost_cpu = cost.to_device(device="cpu")
274        #@NOTE 
275        #   Split the cost matrices into individual samples. Note that `split()` removes the padding,
276        # so that the valid matrices without any filler regions are returned.
277        per_sample_costs = cost_cpu.split()
278        #@NOTE: Perform matching for each sample individually in a loop.
279        matched_gt_list = [None] * batch_size
280        matched_pred_list = [None] * batch_size
281        nvtx_wrp.range_push("assigner loop [HungarianAssigner2DBatched.assign]")
282        for i in range(batch_size):
283            cost_i = per_sample_costs[i]
284
285            nvtx_wrp.range_push("hungarian [HungarianAssigner2DBatched]")
286            matched_row_inds, matched_col_inds = linear_sum_assignment(cost_i)
287            nvtx_wrp.range_pop()
288
289            matched_gt_list[i] = torch.from_numpy(matched_col_inds)
290            matched_pred_list[i] = torch.from_numpy(matched_row_inds)
291        nvtx_wrp.range_pop()
292

Combine Results and Post-processing

After performing the matching, the results are combined into RaggedBatch objects. In addition, note that

  • The original batch shape is restored using the reshape_batch_dims method.

  • The result format is different from the original implementation. Here, we return the matching results as index pairs (in two separate RaggedBatch objects, one containing indices in the predictions, and one containing corresponding indices in the GTs). In the original implementation, an AssignResult object is returned.

demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py
293        # ===== Combine the matching results & post-processing =====
294
295        #@NOTE: Combine the matching results for all samples into a single `RaggedBatch`.
296        gt_matched_indices = bhl.combine_data(matched_gt_list)
297        #@NOTE 
298        #   Note the `other_with_same_sample_sizes` argument, which is used to specify that the
299        # second `RaggedBatch` (here `matched_pred_list`) has the same sample sizes as the first one
300        # (here `gt_matched_indices`). This is not required, but results in a more efficient implementation
301        # as the `sample_sizes` and `mask` tensors can be shared between the two `RaggedBatch` instances.
302        pred_matched_indices = bhl.combine_data(
303            matched_pred_list, other_with_same_sample_sizes=gt_matched_indices
304        )
305
306        #@NOTE: Convert to the expected format and transfer to the correct device.
307        device = bbox_pred.device
308        pred_matched_indices = pred_matched_indices.to_device(device=device)
309        gt_matched_indices = gt_matched_indices.to_device(device=device)
310
311        #@NOTE 
312        # If the batch dimensions were flattened, restore the original shape.
313        # Note that the overall shape of the results is different from the shape of the original.
314        # However, as here, we are only concerned with the batch shape, it is the same and 
315        # we can easily reshape into the needed format.
316        if needs_reshape:
317            pred_matched_indices = pred_matched_indices.reshape_batch_dims(batch_shape_orig)
318            gt_matched_indices = gt_matched_indices.reshape_batch_dims(batch_shape_orig)
319
320        #@NOTE: Types of range pops
321        #@NOTE: A simple range pop to close the NVTX range, without mismatch checking.
322        nvtx_wrp.range_pop()
323        #@NOTE: This will check for range stack mismatches if checking is enabled (enable only for debugging).
324        nvtx_wrp.range_pop("HungarianAssigner2DBatched.assign")
325
326        #@NOTE 
327        #   Return the matching results. In contrast to the original implementation, here, the results
328        # of the matching (i.e. pairs of indices) are returned directly (as `RaggedBatch` instances). This 
329        # is different from the original implementation, where an `AssignResult` instance is returned. 
330        # See the comments in `focal_head_batched.py` for a discussion of the approach used here instead.
331        return pred_matched_indices, gt_matched_indices

Note on the 3D Assigner Implementation

Note that apart from the HungarianAssigner2DBatched implementation, there is also a HungarianAssigner3DBatched implementation, which is used by the StreamPETRHeadBatched class. The implementation is very similar to the HungarianAssigner2DBatched implementation, and so we will not discuss it here. Please refer to the HungarianAssigner3DBatched implementation for more details.