Skip to content

Commit 77d176d

Browse files
chmjkbclaudeNorbertKlockiewicz
authored
release: v0.9.2 (#1259)
## Description Adds a patch with RF-Detr Keypoint preview support ### Introduces a breaking change? - [ ] Yes - [x] No ### Type of change - [ ] Bug fix (change which fixes an issue) - [x] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [ ] Other (chores, tests, code style improvements etc.) ### Tested on - [ ] iOS - [ ] Android ### Testing instructions <!-- Provide step-by-step instructions on how to test your changes. Include setup details if necessary. --> ### Screenshots <!-- Add screenshots here, if applicable --> ### Related issues <!-- Link related issues here using #issue-number --> ### Checklist - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings ### Additional notes <!-- Include any additional information, assumptions, or context that reviewers might need to understand this PR. --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Norbert Klockiewicz <Nklockiewicz12@gmail.com>
1 parent bca2a54 commit 77d176d

11 files changed

Lines changed: 188 additions & 58 deletions

File tree

‎apps/computer-vision/app/pose_estimation/index.tsx‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import Spinner from '../../components/Spinner';
22
import { BottomBar } from '../../components/BottomBar';
3+
import { ModelPicker, ModelOption } from '../../components/ModelPicker';
34
import { getImage } from '../../utils';
45
import {
56
models,
67
usePoseEstimation,
78
PoseDetections,
9+
PoseEstimationModelSources,
810
RnExecutorchError,
911
RnExecutorchErrorCode,
1012
} from 'react-native-executorch';
@@ -17,6 +19,16 @@ import Svg, { Circle, Line } from 'react-native-svg';
1719
import ErrorBanner from '../../components/ErrorBanner';
1820
import { COCO_SKELETON_CONNECTIONS } from '../../components/utils/cocoSkeleton';
1921

22+
const poseEstimation = models.pose_estimation;
23+
24+
const MODELS: ModelOption<PoseEstimationModelSources>[] = [
25+
{ label: 'YOLO26N Pose', value: poseEstimation.yolo26n() },
26+
{
27+
label: 'RF-DETR Keypoint (beta)',
28+
value: poseEstimation.rfdetr_keypoint_preview(),
29+
},
30+
];
31+
2032
// Colors for different people
2133
const PERSON_COLORS = ['lime', 'cyan', 'magenta', 'yellow', 'orange', 'pink'];
2234

@@ -30,8 +42,10 @@ export default function PoseEstimationScreen() {
3042
}>();
3143
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
3244
const [layout, setLayout] = useState({ width: 0, height: 0 });
45+
const [selectedModel, setSelectedModel] =
46+
useState<PoseEstimationModelSources>(poseEstimation.yolo26n());
3347

34-
const model = usePoseEstimation({ model: models.pose_estimation.yolo26n() });
48+
const model = usePoseEstimation({ model: selectedModel });
3549
const { setGlobalGenerating } = useContext(GeneratingContext);
3650

3751
useEffect(() => {
@@ -60,7 +74,7 @@ export default function PoseEstimationScreen() {
6074
if (imageUri) {
6175
try {
6276
const start = Date.now();
63-
const output = await model.forward(imageUri, { inputSize: 384 });
77+
const output = await model.forward(imageUri);
6478
setInferenceTime(Date.now() - start);
6579
setResults(output);
6680
} catch (e) {
@@ -206,6 +220,16 @@ export default function PoseEstimationScreen() {
206220
</View>
207221
)}
208222
</View>
223+
<ModelPicker
224+
models={MODELS}
225+
selectedModel={selectedModel}
226+
disabled={model.isGenerating}
227+
onSelect={(m) => {
228+
setSelectedModel(m);
229+
setResults([]);
230+
setInferenceTime(null);
231+
}}
232+
/>
209233
<StatsBar
210234
inferenceTime={inferenceTime}
211235
detectionCount={results.length > 0 ? results.length : null}

‎docs/docs/03-hooks/02-computer-vision/usePoseEstimation.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,15 @@ See the full guide: [VisionCamera Integration](./visioncamera-integration.md).
129129

130130
## Supported models
131131

132-
| Model | Number of keypoints | Keypoint list | Multi-size Support |
133-
| ------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------- | ------------------ |
134-
| [YOLO26N-Pose](https://huggingface.co/software-mansion/react-native-executorch-yolo26-pose) | 17 | [COCO](../../06-api-reference/enumerations/CocoKeypoint.md) | Yes (384/512/640) |
132+
| Model | Number of keypoints | Keypoint list | Multi-size Support |
133+
| ------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------- | ------------------ |
134+
| [YOLO26N-Pose](https://huggingface.co/software-mansion/react-native-executorch-yolo26-pose) | 17 | [COCO](../../06-api-reference/enumerations/CocoKeypoint.md) | Yes (384/512/640) |
135+
| [RF-DETR Keypoint (preview)](https://huggingface.co/software-mansion/react-native-executorch-rfdetr-keypoint) | 17 | [COCO](../../06-api-reference/enumerations/CocoKeypoint.md) | No |
135136

136137
:::tip
137138
YOLO models support multiple input sizes (384px, 512px, 640px). Smaller sizes are faster but less accurate, while larger sizes are more accurate but slower. Choose based on your speed/accuracy requirements.
138139
:::
140+
141+
:::warning
142+
`rfdetr_keypoint_preview` is a **preview weights** export and may be re-exported under a different constant once a stable version ships. It is a single-input-size model (no `inputSize` option) and ships `xnnpack`, `coreml`, and `mlx` backends — pass `{ backend }` to override the platform default, e.g. `models.pose_estimation.rfdetr_keypoint_preview({ backend: 'mlx' })`.
143+
:::

‎docs/docs/05-utilities/model-registry.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Each leaf is a **function**. Call it (optionally with `{ quant, backend }`) to g
2626
| `classification` | `efficientnet_v2_s` |
2727
| `privacy_filter` | `openai`, `nemotron` |
2828
| `object_detection` | `ssdlite_320_mobilenet_v3_large`, `yolo26n` … `yolo26x`, `rf_detr_nano` |
29-
| `pose_estimation` | `yolo26n` |
29+
| `pose_estimation` | `yolo26n`, `rfdetr_keypoint_preview` _(beta)_ |
3030
| `semantic_segmentation` | `deeplab_v3_resnet50`, `lraspp_mobilenet_v3_large`, `fcn_resnet101`, `selfie_segmentation`, … |
3131
| `instance_segmentation` | `yolo26n` … `yolo26x`, `rf_detr_nano`, `fastsam_s`, `fastsam_x` |
3232
| `style_transfer` | `candy`, `mosaic`, `rain_princess`, `udnie` |

‎packages/react-native-executorch/common/runner/encoders/vision_encoder.cpp‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Result<EValue> VisionEncoder::encode(const MultimodalInput &input) {
112112

113113
auto it = embedding_cache_.find(path);
114114
if (it != embedding_cache_.end()) {
115-
return it->second;
115+
return EValue(*it->second.tensor);
116116
}
117117

118118
auto shape = ET_UNWRAP(getInputShape());
@@ -128,9 +128,18 @@ Result<EValue> VisionEncoder::encode(const MultimodalInput &input) {
128128
chw.data(), sizes, ::executorch::aten::ScalarType::Float);
129129

130130
auto result = ET_UNWRAP(module_->execute(kVisionEncoderMethod, image_tensor));
131-
auto embedding = result[0];
132-
embedding_cache_.emplace(path, embedding);
133-
return embedding;
131+
auto out_tensor = result[0].toTensor();
132+
133+
CachedEmbedding cached;
134+
cached.bytes.resize(out_tensor.nbytes());
135+
std::memcpy(cached.bytes.data(), out_tensor.const_data_ptr(),
136+
out_tensor.nbytes());
137+
cached.sizes.assign(out_tensor.sizes().begin(), out_tensor.sizes().end());
138+
cached.dtype = out_tensor.scalar_type();
139+
auto [entry, inserted] = embedding_cache_.emplace(path, std::move(cached));
140+
entry->second.tensor = ::executorch::extension::from_blob(
141+
entry->second.bytes.data(), entry->second.sizes, entry->second.dtype);
142+
return EValue(*entry->second.tensor);
134143
}
135144

136145
} // namespace executorch::extension::llm

‎packages/react-native-executorch/common/runner/encoders/vision_encoder.h‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
#pragma once
33

44
#include "iencoder.h"
5+
#include <cstdint>
56
#include <executorch/extension/module/module.h>
7+
#include <executorch/extension/tensor/tensor.h>
68
#include <executorch/runtime/core/evalue.h>
79
#include <runner/multimodal_input.h>
810
#include <string>
911
#include <unordered_map>
12+
#include <vector>
1013

1114
namespace executorch::extension::llm {
1215

@@ -26,13 +29,23 @@ class VisionEncoder : public IEncoder {
2629
bool with_batch;
2730
};
2831

32+
// The method's output EValue aliases the runtime's reusable output buffer,
33+
// which the NEXT vision_encoder.execute() overwrites — caching it directly
34+
// silently turns earlier images into the most recently encoded one. Cache
35+
// an owned byte snapshot instead and hand out a tensor over those bytes.
36+
struct CachedEmbedding {
37+
std::vector<uint8_t> bytes;
38+
std::vector<::executorch::aten::SizesType> sizes;
39+
::executorch::aten::ScalarType dtype;
40+
::executorch::extension::TensorPtr tensor;
41+
};
42+
2943
::executorch::runtime::Result<ImageShape> getInputShape() const;
3044
std::vector<float> preprocessImage(const std::string &path,
3145
const ImageShape &targetShape) const;
3246

3347
::executorch::extension::Module *module_;
34-
std::unordered_map<std::string, ::executorch::runtime::EValue>
35-
embedding_cache_;
48+
std::unordered_map<std::string, CachedEmbedding> embedding_cache_;
3649
};
3750

3851
} // namespace executorch::extension::llm

‎packages/react-native-executorch/common/runner/sampler.cpp‎

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
#include <algorithm>
3737
#include <ctime>
3838
#include <limits>
39+
#include <ranges>
40+
#include <span>
41+
#include <type_traits>
3942
#include <vector>
4043

4144
namespace executorch {
@@ -141,57 +144,59 @@ template <typename T> void Sampler::mask_topk(T *logits) {
141144
}
142145
}
143146

144-
// Mask logits whose softmax-prob falls outside the top-p nucleus to -inf.
145-
// Keeps the token that crosses the threshold (HuggingFace convention).
147+
// Mask logits outside the top-p nucleus to -inf. Approximates the exact
148+
// sort-based nucleus with a histogram over (logit - max): two O(n) passes, no
149+
// sort. Binning in logit (not probability) space keeps uniform resolution for
150+
// peaked and flat distributions alike. kRange=40 spans exp() down to ~4e-18.
146151
template <typename T> void Sampler::mask_topp(T *logits) {
147152
if (topp_ <= 0.0f || topp_ >= 1.0f) {
148153
return;
149154
}
150-
// Softmax into a scratch probs[] (do not mutate logits yet).
151-
T max_val = logits[0];
152-
for (size_t i = 1; i < vocab_size_; i++) {
153-
if (logits[i] > max_val) {
154-
max_val = logits[i];
155-
}
156-
}
157-
std::unique_ptr<ProbIndex<T>[]> probindex =
158-
std::make_unique<ProbIndex<T>[]>(vocab_size_);
159-
T sum = 0;
155+
constexpr int32_t kBins = 2048;
156+
// Compute in a type at least as wide as T so converting logits never loses
157+
// precision: double stays double, everything else (float and the narrow
158+
// half/bf16/uint16 logit types) widens to float. Accumulating in T directly
159+
// would be unsafe for bf16, whose mantissa saturates when summing exp()
160+
// over the full vocab.
161+
using acc_t = std::conditional_t<std::is_same_v<T, double>, double, float>;
162+
constexpr acc_t kRange = 40;
163+
164+
std::span<const T> logit_span{logits, static_cast<size_t>(vocab_size_)};
165+
const acc_t max_val =
166+
static_cast<acc_t>(*std::ranges::max_element(logit_span));
167+
168+
std::vector<acc_t> bin_mass(kBins, acc_t(0));
169+
acc_t total = 0;
160170
for (size_t i = 0; i < vocab_size_; i++) {
161-
T e = static_cast<T>(std::expf(static_cast<float>(logits[i] - max_val)));
162-
probindex[i].prob = e;
163-
probindex[i].index = i;
164-
sum += e;
171+
acc_t d = static_cast<acc_t>(logits[i]) - max_val;
172+
acc_t e = std::exp(d);
173+
total += e;
174+
int32_t bin = static_cast<int32_t>((d + kRange) / kRange * kBins);
175+
bin = std::clamp(bin, 0, kBins - 1);
176+
bin_mass[bin] += e;
165177
}
166-
if (sum <= T(0)) {
178+
if (total <= acc_t(0)) {
167179
return;
168180
}
169-
for (size_t i = 0; i < vocab_size_; i++) {
170-
probindex[i].prob /= sum;
171-
}
172-
std::sort(probindex.get(), probindex.get() + vocab_size_,
173-
[](const ProbIndex<T> &a, const ProbIndex<T> &b) {
174-
return a.prob > b.prob;
175-
});
176181

177-
// Find the smallest prefix whose cumulative probability >= topp_.
178-
T cumulative = 0;
179-
int last_idx = vocab_size_ - 1;
180-
for (size_t i = 0; i < vocab_size_; i++) {
181-
cumulative += probindex[i].prob;
182-
if (static_cast<float>(cumulative) >= topp_) {
183-
last_idx = i;
182+
// Highest bin downward until the kept mass reaches topp. The crossing bin is
183+
// kept (HuggingFace "keep the token that crosses" convention).
184+
const acc_t target = static_cast<acc_t>(topp_) * total;
185+
acc_t acc = 0;
186+
int32_t keep_bin = 0;
187+
for (int32_t bin = kBins - 1; bin >= 0; --bin) {
188+
acc += bin_mass[bin];
189+
if (acc >= target) {
190+
keep_bin = bin;
184191
break;
185192
}
186193
}
187-
// Mark kept indices, then -inf the rest.
188-
std::vector<bool> keep(vocab_size_, false);
189-
for (size_t i = 0; i <= last_idx; i++) {
190-
keep[probindex[i].index] = true;
191-
}
194+
const acc_t d_threshold =
195+
static_cast<acc_t>(keep_bin) / kBins * kRange - kRange;
196+
192197
constexpr T neg_inf = std::numeric_limits<T>::lowest();
193198
for (size_t i = 0; i < vocab_size_; i++) {
194-
if (!keep[i]) {
199+
if (static_cast<acc_t>(logits[i]) - max_val < d_threshold) {
195200
logits[i] = neg_inf;
196201
}
197202
}
@@ -210,22 +215,28 @@ Sampler::Sampler(int32_t vocab_size, GenerationConfig config)
210215
: Sampler(vocab_size, config, std::time(nullptr)) {}
211216

212217
template <typename T> static void softmax(T *x, int size) {
213-
// find max value (for numerical stability)
218+
// Runs after top-k/top-p masking, which sets rejected logits to lowest().
219+
// Skip exp() on those: it underflows to 0 anyway and is slow on device.
220+
constexpr T kMasked = std::numeric_limits<T>::lowest();
214221
T max_val = x[0];
215222
for (size_t i = 1; i < size; i++) {
216223
if (x[i] > max_val) {
217224
max_val = x[i];
218225
}
219226
}
220-
// exp and sum
221227
T sum = 0;
222228
for (size_t i = 0; i < size; i++) {
229+
if (x[i] == kMasked) {
230+
x[i] = T(0);
231+
continue;
232+
}
223233
x[i] = expf(x[i] - max_val);
224234
sum += x[i];
225235
}
226-
// normalize
227236
for (size_t i = 0; i < size; i++) {
228-
x[i] /= sum;
237+
if (x[i] != T(0)) {
238+
x[i] /= sum;
239+
}
229240
}
230241
}
231242

‎packages/react-native-executorch/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-executorch",
3-
"version": "0.9.1",
3+
"version": "0.9.2",
44
"description": "An easy way to run AI models in React Native with ExecuTorch",
55
"source": "./src/index.ts",
66
"main": "./lib/module/index.js",

‎packages/react-native-executorch/src/constants/modelRegistry.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,31 @@ const RF_DETR_NANO_SEG_VARIANTS = {
276276
},
277277
};
278278

279+
// RF-DETR Keypoint (pose estimation) — BETA preview. Configs mirror the
280+
// All three backends ship fp32
281+
// (non-quantized); this entry may be re-exported under a different constant
282+
// once more RF-DETR keypoint weights are released.
283+
const RF_DETR_KEYPOINT_PREVIEW_VARIANTS = {
284+
xnnpack: {
285+
base: {
286+
modelName: 'rfdetr-keypoint-preview' as const,
287+
modelSource: M.RF_DETR_KEYPOINT_PREVIEW_XNNPACK_FP32_MODEL,
288+
},
289+
},
290+
coreml: {
291+
base: {
292+
modelName: 'rfdetr-keypoint-preview' as const,
293+
modelSource: M.RF_DETR_KEYPOINT_PREVIEW_COREML_FP32_MODEL,
294+
},
295+
},
296+
mlx: {
297+
base: {
298+
modelName: 'rfdetr-keypoint-preview' as const,
299+
modelSource: M.RF_DETR_KEYPOINT_PREVIEW_MLX_FP32_MODEL,
300+
},
301+
},
302+
};
303+
279304
const FASTSAM_S_VARIANTS = {
280305
xnnpack: {
281306
base: {
@@ -553,6 +578,9 @@ export const models = {
553578
},
554579
pose_estimation: {
555580
yolo26n: base(M.YOLO26N_POSE),
581+
// BETA preview — may be re-exported under a different constant once a
582+
// stable RF-DETR keypoint model ships.
583+
rfdetr_keypoint_preview: variant(RF_DETR_KEYPOINT_PREVIEW_VARIANTS),
556584
},
557585
semantic_segmentation: {
558586
deeplab_v3_resnet50: pair(

‎packages/react-native-executorch/src/constants/modelUrls.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,28 @@ export const YOLO26N_POSE = {
731731
modelSource: YOLO26N_POSE_MODEL,
732732
} as const;
733733

734+
// RF-DETR Keypoint (pose estimation) — BETA preview.
735+
// NOTE: served from the `preview/` path under PREVIOUS_VERSION_TAG (shipping as
736+
// part of a patch release). This export is a preview and may be re-exported
737+
// under a different constant once a stable version ships.
738+
export const RF_DETR_KEYPOINT_PREVIEW_XNNPACK_FP32_MODEL = `${URL_PREFIX}-rfdetr-keypoint/${VERSION_TAG}/preview/xnnpack/rfdetr_keypoint_preview_xnnpack_fp32.pte`;
739+
export const RF_DETR_KEYPOINT_PREVIEW_COREML_FP32_MODEL = `${URL_PREFIX}-rfdetr-keypoint/${VERSION_TAG}/preview/coreml/rfdetr_keypoint_preview_coreml_fp32.pte`;
740+
export const RF_DETR_KEYPOINT_PREVIEW_MLX_FP32_MODEL = `${URL_PREFIX}-rfdetr-keypoint/${VERSION_TAG}/preview/mlx/rfdetr_keypoint_preview_mlx_fp32.pte`;
741+
const RF_DETR_KEYPOINT_PREVIEW_MODEL =
742+
Platform.OS === 'ios'
743+
? RF_DETR_KEYPOINT_PREVIEW_COREML_FP32_MODEL
744+
: RF_DETR_KEYPOINT_PREVIEW_XNNPACK_FP32_MODEL;
745+
746+
/**
747+
* @category Models - Pose Estimation
748+
* @beta Preview export — may be re-exported under a different constant once a
749+
* stable RF-DETR keypoint model ships.
750+
*/
751+
export const RF_DETR_KEYPOINT_PREVIEW = {
752+
modelName: 'rfdetr-keypoint-preview',
753+
modelSource: RF_DETR_KEYPOINT_PREVIEW_MODEL,
754+
} as const;
755+
734756
// Style transfer
735757
/**
736758
* Builds the four `(backend, precision)` URLs for a single style-transfer style.

0 commit comments

Comments
 (0)