| --- |
| license: apache-2.0 |
| tags: |
| - executorch |
| - xnnpack |
| - pte |
| - on-device |
| - keypoint-detection |
| - pose-estimation |
| --- |
| # RTMW-m (whole-body pose, 133 keypoints) β ExecuTorch |
|
|
| `rtmw_m_xnnpack_fp32.pte` (129.1 MB) |
|
|
| - **Source**: [mmpose](https://github.com/open-mmlab/mmpose) RTMW, |
| `rtmw-dw-l-m_simcc-cocktail14_270e-256x192` (the m-size distilled build) |
| - **License**: Apache-2.0 |
| - **Input**: `[1, 3, 256, 192]`, a crop around one person, ImageNet-normalised |
| - **Output**: two SimCC maps, `[1, 133, 384]` and `[1, 133, 512]` β the x and y bins for each |
| keypoint at a split ratio of 2.0 |
|
|
| Body, feet, face and both hands in one pass. The shelf's four RTMPose variants each cover |
| one of those; this covers all of them, which is the whole reason to carry a model this size |
| next to a 21.9 MB body-only one. |
|
|
| Top-down: it expects a box around a person, not a scene. Pair it with a detector β the shelf |
| has [YOLOX-s](https://huggingface.co/mlboydaisuke/YOLOX-s-ExecuTorch). |
|
|
| ## Verification (Mac arm64, 2026-08-23) |
|
|
| XNNPACK fp32: **17.0 ms** against 178.9 ms eager, worst corr 1.000000, 129.1 MB. |
|
|
| Correlation is measured on random input, so the check that matters here is decoded |
| coordinates on real person crops β 133 keypoints across five crops, 665 measurements, |
| against eager on the same tensor: |
|
|
| | build | mean apart | over 2 px | over 10 px | worst | |
| |---|---|---|---|---| |
| | **XNNPACK fp32** | **0.001 px** | 0 | 0 | 0.5 px | |
| | XNNPACK fp16 | 0.414 px | 4 | 2 | 123 px | |
| | Core ML | 1.199 px | 54 | 4 | 124.5 px | |
|
|
| fp32 is exact on four of the five crops; the one difference is half a pixel, which is a |
| single SimCC bin. |
|
|
| A worst of 123 px on a 192-wide crop looks alarming and is worth reading with the count |
| beside it: for fp16 that is 4 of 665 keypoints, and those four had the lowest peak |
| activations in eager's own output (median summed peak 0.39). SimCC picks a bin by argmax, so |
| a keypoint the model is unsure about flips to a distant bin under any numeric change. Core |
| ML moves 54 of 665, which is a different matter. |
|
|
| ## Not shipped, and why |
|
|
| - **fp16**: 127.9 MB against 129.1, and **54.2 ms against 17.0** β three times slower for no |
| size gain. The keypoint agreement above is good; there is simply nothing to buy. |
| - **Core ML**: worst corr 0.920, below the 0.995 gate, and 54 of 665 keypoints move. |
| - **int8**: does not export. PT2E quantization fails with `ValueError: getitem_6 is not an |
| aten adaptive_avg_pool2d operator` β the quantizer's pattern match against CSPNeXt's |
| channel attention, reached through the PAFPN's tuple output. The RTMPose variants on this |
| shelf have no int8 either, though those failed on quality rather than at export. |
| |
| ## Conversion |
| |
| ```bash |
| CONVERT_REPOS=<dir containing mmpose> python convert/export_rtmw.py fp32 |
| CONVERT_REPOS=<dir containing mmpose> python convert/check_rtmw.py |
| ``` |
| |
| RTMW is not another entry in `export_rtmpose.py`'s variant table: RTMPose feeds the |
| backbone's last stage straight to the head, RTMW puts a `CSPNeXtPAFPN` neck in between and |
| uses `RTMWHead`. The widths and sizes are read off mmpose's own |
| `rtmw-m_8xb1024-270e_cocktail14-256x192.py`, and a strict `load_state_dict` is what proves |
| they match the checkpoint. mmpose needs `mmengine` and `mmcv-lite`; `xtcocotools`, `mmdet` |
| and `mmcv._ext` are imported at module scope and never called on this path, so they are |
| stubbed. |
|
|
| **Feed it contiguous tensors.** ExecuTorch reads a tensor in memory order rather than by its |
| strides, and a crop built with `np.transpose` is not contiguous. Random input is, which is |
| why the correlation gate cannot see the difference. |
|
|
| (conversion scripts: [executorch-models](https://github.com/john-rocky/executorch-models)) |
|
|