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.pyHungarianAssigner2D (non-batched reference):
demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d.pyFocalHeadBatched:
demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.pyFocalHead (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.)
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.
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.
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_dimsmethod.The result format is different from the original implementation. Here, we return the matching results as index pairs (in two separate
RaggedBatchobjects, one containing indices in the predictions, and one containing corresponding indices in the GTs). In the original implementation, anAssignResultobject is returned.
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.