Repository navigation
Description
Related to #2382 (listed there; scheduled outside its DAG because it rides mlxcel detect)
Problem / Background
RF-DETR (Roboflow, ICLR 2026) is a real-time closed-vocabulary detector on a windowed DINOv2 backbone with a C2f multi-scale projector, two-stage query selection and a deformable decoder, with an optional instance-segmentation head. Its output is boxes, COCO labels and scores (plus masks for the seg variants), which is exactly the mlxcel detect contract RT-DETRv2 already uses; #524 ranked it as the cheapest family there to re-scope for that reason. mlxcel cannot load it today.
Surface decision: RF-DETR registers in mlxcel detect beside rt_detr_v2 through the Detector trait and load_detector dispatch that #2412 introduces (#2413 adds the same contract if it lands first, and changes threshold to Option<f32>), not in the #2349 predict surface. Masks from the segmentation variants leave detect through a new --masks-dir <DIR> flag (one PNG per detection, path recorded in the JSON output). Rejected alternative: routing the segmentation variants through mlxcel predict, which would give one model two commands and make box-only users depend on the dense-prediction surface.
Upstream reference
- Model directory: https://github.com/Blaizzy/mlx-vlm/tree/main/mlx_vlm/models/rfdetr (README). PRs: Add RF-DETR detection and segmentation model Neywa-Labs/mlx-vlm#884 (model), Use fused SDPA in the RF-DETR backbone attention Neywa-Labs/mlx-vlm#2443 (fused SDPA in backbone attention), Fix off-by-one RF-DETR backbone feature layers Neywa-Labs/mlx-vlm#2450 (off-by-one backbone feature layers), Make RF-DETR sanitize idempotent Neywa-Labs/mlx-vlm#2452 (idempotent sanitize), Fix RF-DETR small variant config Neywa-Labs/mlx-vlm#2453 (small variant config), Use the separable interpolation kernel for the RF-DETR segmentation u… Neywa-Labs/mlx-vlm#2468 (separable interpolation for the segmentation upsample).
- Backbone and projector:
vision.py(DINOv2Embeddings.interpolate_pos_encodingL30,DINOv2Attention,DINOv2Layerwith layer scale,_window_partition/_window_unpartitionL204-L250,DINOv2BackboneL251-L318: layers not inout_feature_indexesattend withinnum_windows^2windows (each with its own CLS), the others merge windows for global attention, and normed patch tokens fromout_feature_indexesare unpartitioned into feature maps;C2f,Bottleneck,ConvBN,MultiScaleProjectorL320-L428). - Decoder:
transformer.py(gen_sineembed_for_position,_gen_encoder_output_proposals,MSDeformableAttentionL91-L213,DecoderLayer,Decoder,Transformer.two_stage_selectL417-L489;group_detr13 is a training-time query grouping, inference usesnum_queries). - Heads:
rfdetr.py#L56-L106(bbox_reparam:cxcy = d_xy * ref_wh + ref_xy,wh = exp(d_wh) * ref_wh),segmentation.py(SegmentationHead: bilinear resize of projector features toimage / downsample_ratiowithalign_corners=True, depthwise conv blocks, einsumbhwc,bnc->bnhwplus a learned bias),sanitizeatrfdetr.py#L108-L177(key remaps,in_projsplits, conv transpose only when the shape check says the weight is not already MLX layout). - Pre- and post-processing:
processing_rfdetr.py#L154-L178(RGB, PIL bilinear resize toresolutionxresolution, scale to [0, 1], ImageNet mean and std frompreprocessor_config.json),COCO_CLASSES(91 entries starting withN/A);generate.py#L40-L220(sigmoid, per-query max over classes, topnum_select300, keepscore > threshold,cxcywhtoxyxyscaled and clipped to the source size, per-class greedy NMS at IoU 0.5 sorted by score, masks for surviving queries resized with cubic interpolation and thresholded> 0); predictor defaultsscore_threshold0.5,nms_threshold0.5 (L222-L240). - Checkpoints (MLX layout with
config.jsonmodel_type: "rf-detr",preprocessor_config.json):mlx-community/rfdetr-base-fp32(detection, resolution 560, patch 14,out_feature_indexes [2, 5, 8, 11], 3 decoder layers, 300 queries),mlx-community/rfdetr-seg-small-fp32(segmentation, resolution 384, patch 12,out_feature_indexes [3, 6, 9, 12], 4 decoder layers, 100 queries), plusrfdetr-seg-large,-xlarge,-2xlargevariants. Note that the two variants differ in patch size and feature indexes; both must load.
Current Behavior
src/commands/detect.rs:30run_detecthard-codesRtDetrV2Predictor::from_pretrainedat:55;sort_reading_orderis at:141.DetectArgs(src/main.rs:890-906) has--model,--image,--threshold(default 0.3) and--format.src/vision/detection/mod.rs:28-31re-exports only RT-DETRv2 types;Detection(src/vision/detection/rt_detr_v2/predictor.rs:37-47) isbbox,score,label,class_name.- Reusable RT-DETRv2 pieces: bilinear
grid_sample(src/vision/detection/rt_detr_v2/layers.rs),inverse_sigmoidandMlp(src/vision/detection/rt_detr_v2/transformer.rs:35and:50), the deformable sampling loop (transformer.rs:88-230), and image decoding plus PIL-equivalent bilinear resize throughimage::imageops::FilterType::Triangle(src/vision/detection/rt_detr_v2/processor.rs:154-180). - feat(detect): Add PP-DocLayoutV3 layout detection with reading order #2412 and feat(detect): Add YOLO11 detector for OmniParser v2 icon detection #2413 (both open) each specify the
Detectortrait,load_detectordispatch onconfig.jsonmodel_type,Detectionmoved tomod.rs, and per-detector default thresholds; whichever lands second registers into the first one's dispatch. - The feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349
src/vision/dense/interpolate.rsport (open) providesresize_bilinear_nhwc(withalign_corners) andresize_bicubic_nhwc, matching upstreammlx_vlm/models/interpolate.py, which RF-DETR's segmentation head imports. - feat: Port DINOv2 backbone (dinov2, dinov2_with_registers) for dense models #2350 ports a standard DINOv2 and explicitly leaves the RF-DETR windowed variant out of scope.
Proposed Solution
New module src/vision/detection/rfdetr/ (config.rs, backbone.rs, projector.rs, transformer.rs, segmentation.rs, sanitize.rs, predictor.rs, tests.rs).
- Dispatch: add an
rf-detrarm (also acceptrfdetr, upstream's remap target) toload_detector; implementDetectorforRfDetrPredictor. Default threshold 0.5 (upstream predictor) when--thresholdis unset. backbone.rs: the windowed DINOv2 as upstream (own module, not feat: Port DINOv2 backbone (dinov2, dinov2_with_registers) for dense models #2350'sDinov2: per-window CLS tokens and window merge/split around global layers have no counterpart there). Position embedding interpolation uses feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349resize_bicubic_nhwcthe way upstreaminterpolate_pos_encodingdoes.transformer.rs: two-stage selection and deformable decoder, reusing RT-DETRv2'sgrid_sample,inverse_sigmoid,Mlpand deformable sampling (move any shared function to a common location only if needed, and add// Used by: RT-DETRv2, RF-DETR).segmentation.rs: the head, using feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349resize_bilinear_nhwc(.., align_corners: true); if feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349 shipped that function without thealign_cornersparameter, add it there with upstream's weight computation rather than writing a local resize.predictor.rs: preprocessing through the RT-DETRv2 processor's decode and Triangle resize with RF-DETR's resolution, mean and std; post-processing as upstream; class names fromCOCO_CLASSES(ported as a constant) unlessconfig.jsoncarriesid2label. NMS IoU defaults to 0.5 and honors--iouwhen feat(detect): Add YOLO11 detector for OmniParser v2 icon detection #2413 has added it.- Masks: add
mask: Option<image::GrayImage>toDetection(Nonefor every other detector; update their constructors). Masks upsample fromimage / 4logits to the source size with feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349resize_bicubic_nhwc(antialias off, matching the upstream cubic upscale) and threshold> 0. NewDetectArgsflag--masks-dir <DIR>: creates the directory, writesmask-000.png,mask-001.png, ... (8-bit single channel, 0 or 255, source resolution, numbered in the printed order aftersort_reading_order), and adds"mask": "<path>"to each JSON detection. Without the flag no mask is written; with the flag on a model that has no segmentation head,detecterrors before inference. The PNG format matches the SAM 3 masks in feat: Port SAM 3 (sam3) promptable segmentation and video tracking #2429. sanitize.rs: port the upstream remaps; transpose 4-D convs only when not already(O, H, W, I), idempotent (Make RF-DETR sanitize idempotent Neywa-Labs/mlx-vlm#2452), using the feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349 shared guard.- Registry: add
rf-detrto the standalone family table (src/models/registry.rs:185-204) with output kindboxes.
Scope
In scope: the module above, dispatch registration, Detection.mask, --masks-dir, docs (docs/supported-models.md detection section, the detect CLI docs).
Out of scope: conversion from Roboflow .pth checkpoints (needs PyTorch; the MLX checkpoints load directly), video and realtime modes, exclude_classes, annotators, exposing RF-DETR through mlxcel predict or the server.
Implementation Notes
- Reuse: RT-DETRv2
grid_sample,inverse_sigmoid,Mlp, deformable sampling, image decode and resize; feat: Add dense-prediction output surface: mlxcel predict, map and keypoint files #2349interpolate.rsand conv guard; the feat(detect): Add PP-DocLayoutV3 layout detection with reading order #2412 / feat(detect): Add YOLO11 detector for OmniParser v2 icon detection #2413Detectortrait and dispatch. - Constraints: RT-DETRv2 (and PP-DocLayoutV3 / YOLO11 if landed) output must not change; their existing tests stay green and their JSON gains no
maskfield. - Precision: checkpoints are f32; keep f32 (no bf16 to convert).
- Edge cases: zero detections above threshold (empty list, no files written even with
--masks-dir); classes indexing pastCOCO_CLASSESprintclass_<id>as upstream; a non-square source image (resize is square, boxes rescale per axis). - Error handling: unknown variant shapes fail at load naming the tensor; an unwritable
--masks-dirfails before loading weights.
Acceptance Criteria
-
mlxcel detect -m models/rfdetr-base-fp32 -i street.jpg --format jsonprints COCO detections in pixell, t, r, b. - Raw-output parity against mlx-vlm on
mlx-community/rfdetr-base-fp32for one committed image at 560x560:pred_logitsandpred_boxesmax abs error within 1e-3 in f32; final detections after NMS match (same labels, boxes within 1 px). It fails on current code. - Segmentation:
mlxcel detect -m models/rfdetr-seg-small-fp32 -i street.jpg --format json --masks-dir out/writes one PNG per detection and the JSONmaskpaths; per-mask IoU against upstream at least 0.98. - Unit tests, each failing on current code:
_window_partition/_window_unpartitionround-trip; per-class NMS on synthetic boxes against upstream_nms_per_class;bbox_reparamdecode on a hand-computed case; sanitize idempotence;--masks-diron a detection-only model errors. - RT-DETRv2
detectoutput is unchanged (existingdetectand RT-DETRv2 tests pass). -
mlxcel arch --jsonlistsrf-detr; docs updated.
Verification
cargo build --release --features metal,accelerate
cargo test --workspace --profile test-fast --features metal,accelerate rfdetr
cargo test --workspace --profile test-fast --features metal,accelerate rt_detr
cargo test --workspace --profile test-fast --features metal,accelerate detect
cargo test --workspace --profile test-fast --features metal,accelerate
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
./target/release/mlxcel detect -m models/rfdetr-seg-small-fp32 -i /path/to/street.jpg --format json --masks-dir /tmp/rfdetr-masksDependencies
Blocked by #2412 (Detector trait and load_detector dispatch; #2413 provides the same contract if it merges first, in which case only that one is required) and #2349 (interpolate.rs resize functions and the conv-permutation guard).
Activity
feat: add RF-DETR detection and segmentation
docs: consolidate final vision family contracts
feat: add RF-DETR detection and instance masks (lablup#2581)