Skip to content

Commit 5fddd90

Browse files
committed
refactor(nlp): address review feedback on tokenizer pipeline
- install loadTokenizer under the nlp JSI submodule (rnexecutorchJsi.nlp) - move ops/tokenizer.ts to nlp/tokenizer.ts; standard-template JSDoc, drop low/high-level coupling and Model/Tensor mention - default skipSpecialTokens and empty-input handling in C++ decode - minimal tokenization task (no worklet wrappers; sync lightweight lookups); inline single-field config to a path string - readable tokenizer error strings; drop unnecessary build-config comments
1 parent 81d7ab1 commit 5fddd90

12 files changed

Lines changed: 162 additions & 167 deletions

File tree

packages/react-native-executorch/android/CMakeLists.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
3737
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
3838
../cpp
3939
${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include
40-
# pytorch/tokenizers headers (and the third-party libs they pull in:
41-
# nlohmann/json, re2 and its abseil dependency) ship inside the ExecuTorch
42-
# llm extension bundle
4340
${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/include
4441
${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/third-party/json/include
4542
${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/third-party/re2

packages/react-native-executorch/cpp/extensions/nlp/install.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
#include "tokenizer.h"
33

44
namespace rnexecutorch::extensions::nlp {
5+
namespace jsi = facebook::jsi;
6+
57
void install(facebook::jsi::Runtime &rt, facebook::jsi::Object &module) {
6-
tokenizer::install_loadTokenizer(rt, module);
8+
jsi::Object nlpModule = jsi::Object(rt);
9+
10+
tokenizer::install_loadTokenizer(rt, nlpModule);
11+
12+
module.setProperty(rt, "nlp", nlpModule);
713
}
814
} // namespace rnexecutorch::extensions::nlp

packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ namespace {
1414
// (i.e. special tokens are added exactly as configured in tokenizer.json).
1515
constexpr uint64_t kNumAddedBosTokens = 0;
1616
constexpr uint64_t kNumAddedEosTokens = 0;
17+
18+
// tokenizers::Error is its own enum (not executorch::runtime::Error), and the
19+
// tokenizers library ships no to_string for it, so map it to a readable name.
20+
std::string toString(tokenizers::Error error) {
21+
switch (error) {
22+
case tokenizers::Error::Ok:
23+
return "Ok";
24+
case tokenizers::Error::Internal:
25+
return "Internal";
26+
case tokenizers::Error::Uninitialized:
27+
return "Uninitialized";
28+
case tokenizers::Error::OutOfRange:
29+
return "OutOfRange";
30+
case tokenizers::Error::LoadFailure:
31+
return "LoadFailure";
32+
case tokenizers::Error::EncodeFailure:
33+
return "EncodeFailure";
34+
case tokenizers::Error::Base64DecodeFailure:
35+
return "Base64DecodeFailure";
36+
case tokenizers::Error::ParseFailure:
37+
return "ParseFailure";
38+
case tokenizers::Error::DecodeFailure:
39+
return "DecodeFailure";
40+
case tokenizers::Error::RegexFailure:
41+
return "RegexFailure";
42+
}
43+
return "Unknown(" + std::to_string(static_cast<int32_t>(error)) + ")";
44+
}
1745
} // namespace
1846

1947
TokenizerHostObject::TokenizerHostObject(const std::string &tokenizerPath)
@@ -22,7 +50,7 @@ TokenizerHostObject::TokenizerHostObject(const std::string &tokenizerPath)
2250
auto error = tokenizer_->load(tokenizerPath_);
2351
if (error != tokenizers::Error::Ok) {
2452
throw std::runtime_error("Failed to load tokenizer from '" + tokenizerPath_ +
25-
"': error " + std::to_string(static_cast<int32_t>(error)));
53+
"': " + toString(error));
2654
}
2755
}
2856

@@ -56,8 +84,8 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
5684
auto text = args[0].asString(rt).utf8(rt);
5785
auto result = self->tokenizer_->encode(text, kNumAddedBosTokens, kNumAddedEosTokens);
5886
if (!result.ok()) {
59-
throw jsi::JSError(rt, "encode: Failed to encode input: error " +
60-
std::to_string(static_cast<int32_t>(result.error())));
87+
throw jsi::JSError(rt, "encode: Failed to encode input: " +
88+
toString(result.error()));
6189
}
6290

6391
const auto &ids = result.get();
@@ -74,16 +102,21 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
74102
if (nameStr == "decode") {
75103
auto self = shared_from_this();
76104
auto fnBody = [self](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
77-
if (count != 2) {
78-
throw jsi::JSError(rt, "decode: Usage: decode(tokens, skipSpecialTokens)");
105+
if (count < 1 || count > 2) {
106+
throw jsi::JSError(rt, "decode: Usage: decode(tokens, skipSpecialTokens?)");
79107
}
80108

81109
if (!args[0].isObject() || !args[0].asObject(rt).isArray(rt)) {
82110
throw jsi::JSError(rt, "decode: Expected arg0 to be an array");
83111
}
84112

85-
if (!args[1].isBool()) {
86-
throw jsi::JSError(rt, "decode: Expected arg1 to be a boolean");
113+
// skipSpecialTokens is optional and defaults to true.
114+
bool skipSpecialTokens = true;
115+
if (count == 2 && !args[1].isUndefined()) {
116+
if (!args[1].isBool()) {
117+
throw jsi::JSError(rt, "decode: Expected arg1 to be a boolean");
118+
}
119+
skipSpecialTokens = args[1].asBool();
87120
}
88121

89122
std::unique_lock<std::mutex> lock(self->mutex_, std::try_to_lock);
@@ -96,7 +129,6 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
96129
}
97130

98131
auto tokensArray = args[0].asObject(rt).asArray(rt);
99-
auto skipSpecialTokens = args[1].asBool();
100132

101133
std::vector<uint64_t> tokens;
102134
tokens.reserve(tokensArray.size(rt));
@@ -108,15 +140,19 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
108140
tokens.push_back(static_cast<uint64_t>(val.asNumber()));
109141
}
110142

143+
if (tokens.empty()) {
144+
return jsi::String::createFromUtf8(rt, "");
145+
}
146+
111147
auto result = self->tokenizer_->decode(tokens, skipSpecialTokens);
112148
if (!result.ok()) {
113-
throw jsi::JSError(rt, "decode: Failed to decode tokens: error " +
114-
std::to_string(static_cast<int32_t>(result.error())));
149+
throw jsi::JSError(rt, "decode: Failed to decode tokens: " +
150+
toString(result.error()));
115151
}
116152

117153
return jsi::String::createFromUtf8(rt, result.get());
118154
};
119-
return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "decode"), 2, fnBody);
155+
return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "decode"), 1, fnBody);
120156
}
121157

122158
if (nameStr == "getVocabSize") {
@@ -163,8 +199,8 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
163199
auto tokenId = static_cast<uint64_t>(args[0].asNumber());
164200
auto result = self->tokenizer_->id_to_piece(tokenId);
165201
if (!result.ok()) {
166-
throw jsi::JSError(rt, "idToToken: Failed to convert id to token: error " +
167-
std::to_string(static_cast<int32_t>(result.error())));
202+
throw jsi::JSError(rt, "idToToken: Failed to convert id to token: " +
203+
toString(result.error()));
168204
}
169205

170206
return jsi::String::createFromUtf8(rt, result.get());
@@ -195,8 +231,8 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam
195231
auto token = args[0].asString(rt).utf8(rt);
196232
auto result = self->tokenizer_->piece_to_id(token);
197233
if (!result.ok()) {
198-
throw jsi::JSError(rt, "tokenToId: Failed to convert token to id: error " +
199-
std::to_string(static_cast<int32_t>(result.error())));
234+
throw jsi::JSError(rt, "tokenToId: Failed to convert token to id: " +
235+
toString(result.error()));
200236
}
201237

202238
return static_cast<double>(result.get());

packages/react-native-executorch/react-native-executorch.podspec

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ Pod::Spec.new do |s|
3232
"HEADER_SEARCH_PATHS" => [
3333
"\"$(PODS_TARGET_SRCROOT)/cpp\"",
3434
"\"$(PODS_TARGET_SRCROOT)/third-party/include\"",
35-
# pytorch/tokenizers headers (and the third-party libs they pull in:
36-
# nlohmann/json, re2 and its abseil dependency) ship inside the ExecuTorch
37-
# llm extension bundle
3835
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/include\"",
3936
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/third-party/json/include\"",
4037
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/third-party/re2\"",
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './ops/tokenizer';
2-
export * from './tasks/tokenizer';
1+
export * from './tokenizer';
2+
export * from './tasks/tokenization';

packages/react-native-executorch/src/extensions/nlp/ops/tokenizer.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { WorkletRuntime } from 'react-native-worklets';
2+
3+
import { wrapAsync } from '../../../core/runtime';
4+
import { loadTokenizer } from '../tokenizer';
5+
6+
/**
7+
* Loads a tokenizer and exposes its operations with lifetime management, for
8+
* use by the `useTokenizer` hook and as a building block for higher-level NLP
9+
* tasks (embeddings, privacy filter, ...).
10+
* @category Typescript API
11+
* @param tokenizerPath Absolute local path to a `tokenizer.json` file.
12+
* @param runtime Optional worklet runtime thread to run the tokenizer on.
13+
* @returns A promise resolving to the tokenizer operations and a `dispose`
14+
* handle that releases the native tokenizer.
15+
*/
16+
export async function createTokenizer(tokenizerPath: string, runtime?: WorkletRuntime) {
17+
const tokenizer = await wrapAsync(loadTokenizer, runtime)(tokenizerPath);
18+
const dispose = () => tokenizer.dispose();
19+
20+
return {
21+
dispose,
22+
encode: wrapAsync(tokenizer.encode, runtime),
23+
decode: wrapAsync(tokenizer.decode, runtime),
24+
getVocabSize: tokenizer.getVocabSize,
25+
idToToken: tokenizer.idToToken,
26+
tokenToId: tokenizer.tokenToId,
27+
};
28+
}

packages/react-native-executorch/src/extensions/nlp/tasks/tokenizer.ts

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { rnexecutorchJsi } from '../../native/bridge';
2+
3+
declare const tokenizerBrand: unique symbol;
4+
5+
/**
6+
* A native HuggingFace-compatible tokenizer instance backed by a JSI host
7+
* object. All methods are synchronous and worklet-compatible.
8+
* @category Types
9+
*/
10+
export type Tokenizer = {
11+
/** Absolute local path of the loaded `tokenizer.json`. */
12+
readonly path: string;
13+
14+
/**
15+
* Encodes a string into token ids (special tokens are added according to the
16+
* tokenizer.json post_processor).
17+
* @param text The input text to tokenize.
18+
* @returns The encoded token ids.
19+
*/
20+
encode(text: string): number[];
21+
22+
/**
23+
* Decodes token ids back into a string.
24+
* @param tokens The token ids to decode.
25+
* @param skipSpecialTokens Whether to omit special tokens. Defaults to `true`.
26+
* @returns The decoded text.
27+
*/
28+
decode(tokens: number[], skipSpecialTokens?: boolean): string;
29+
30+
/**
31+
* @returns The size of the tokenizer's vocabulary.
32+
*/
33+
getVocabSize(): number;
34+
35+
/**
36+
* @param id The token id to look up.
37+
* @returns The token string for the given id.
38+
*/
39+
idToToken(id: number): string;
40+
41+
/**
42+
* @param token The token string to look up.
43+
* @returns The id for the given token string.
44+
*/
45+
tokenToId(token: string): number;
46+
47+
/** Releases the native tokenizer. The instance must not be used afterwards. */
48+
dispose(): void;
49+
50+
/**
51+
* Prevents plain JS objects from being cast as Tokenizers.
52+
* @internal
53+
*/
54+
readonly [tokenizerBrand]: never;
55+
};
56+
57+
/**
58+
* Loads a HuggingFace tokenizer from a local `tokenizer.json` file.
59+
* @category Typescript API
60+
* @param tokenizerPath Absolute local path to a `tokenizer.json` file.
61+
* @returns The loaded tokenizer.
62+
*/
63+
export function loadTokenizer(tokenizerPath: string): Tokenizer {
64+
'worklet';
65+
return rnexecutorchJsi.nlp.loadTokenizer(tokenizerPath) as Tokenizer;
66+
}

0 commit comments

Comments
 (0)