Skip to main content

add-task-pipeline

Use when creating a TypeScript task pipeline, implementing image preprocessing/postprocessing, loading models, or wrapping pipelines in React hooks.

Informações da origem

Repositório
software-mansion/react-native-executorch
Última atividade na origem
5 de setembro de 2026 às 17:39
Idioma detectado do SKILL.md
inglês
Estrelas
1.750
Forks
97

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
add-task-pipeline
description
Use when creating a TypeScript task pipeline, implementing image preprocessing/postprocessing, loading models, or wrapping pipelines in React hooks.
metadata
{"id":"add_task_pipeline","scope":"src/extensions/*/tasks/*, src/hooks/*"}
# Skill: Add a High-Level Task Pipeline (TypeScript) Use this guide to construct end-to-end task pipelines (e.g. classification, style transfer, object detection) in TypeScript and wrap them in React hooks. --- ## 🚦 Design Principles When implementing task constructors like `create<Task>` (e.g. `createClassifier`, `createStyleTransfer`), adhere to the following rules: 1. **Pre-allocating Static Tensors (`as const`)**: - Statically sized scratch/output tensors required for inference should be pre-allocated inside the constructor body. - Allocate them using: ```typescript const tensors = [tensor('float32', shapeA), tensor('float32', shapeB)] as const; ``` - **Destructuring & Naming**: Destructure and name the individual tensors immediately after allocation. Always prefix tensor variables with a lowercase `t` (e.g. `tReshape`, `tUint8`, `tInput`) to easily distinguish them from raw data buffers. ```typescript const [tReshape, tUint8] = tensors; ``` 2. **Allocate Through a Resource Scope**: - Open a `createResourceScope()` as the first statement of the constructor, take its `dispose` as the pipeline's, and wrap the whole body in `try`/`catch`. `track` every native resource as it is created: ```typescript const scope = createResourceScope(); const dispose = scope.dispose; try { const model = scope.track(await wrapAsync(loadModel, runtime)(modelPath)); const { dims } = validateSpec(model.schema, { ... }); // may throw const tensors = [tensor('float32', shapeA), tensor('float32', shapeB)] as const; tensors.forEach(scope.track); const preprocessor = scope.track(createImagePreprocessor(modelOpts, inpShape)); return { runTask, runTaskWorklet, dispose }; } catch (error) { dispose(); throw error; } ``` - This is not style. A caller whose `create<Task>` throws never receives a `dispose`, so anything already allocated would be stranded in native memory for the life of the process, and `useModel` re-runs the factory on every config change. The scope makes success and failure share one teardown path. - Track resources **as they land**, not afterwards. For parallel loads that means `Promise.all([load(a).then(scope.track), load(b).then(scope.track)])`, so one rejecting does not strand the other. 3. **Dynamic Tensors & `try/finally` Pattern**: - If you must allocate dynamically sized tensors during inference execution (e.g. resizing an output tensor to match the input image dimensions), you must wrap the execution inside a `try {} finally {}` block. - Dispose of the dynamic tensors inside the `finally` block to prevent native memory leaks. ```typescript const tResize = tensor('uint8', [input.height, input.width, 4]); try { // Perform work... } finally { tResize.dispose(); } ``` 4. **Pure Helper Functions**: - Write all auxiliary/helper logic as pure, worklet-compatible functions **outside** the `create<Task>` constructor. Any helper functions invoked inside the worklet executor thread must contain the `'worklet';` directive. - **Push Back Hard on Inner Helpers:** You must push back hard against any request to add internal closures or nested functions inside `create<Task>` (other than the worklet executor itself). Keep the constructor scope flat to avoid scope leak and dependency chain bugs. 5. **PTE Model Export & Optimizations**: - **Shift Heavy Ops to PyTorch**: Push complex tensor reshaping, data normalization, activations (e.g. `softmax`), or bounding box decoding into the PyTorch model itself so they execute on native backends (e.g., XNNPACK or CoreML). - **Balance Optimization with Generalization**: Keep contracts generic (e.g., normal dense logits, standard bounding box layouts like `xyxy`/`xywh`, standard floating-point arrays). - Handle model-specific configuration parameters (such as unique normalization factors, thresholds, or label arrays) dynamically through the TypeScript task options argument rather than baking them rigidly into JSI C++ code or the model structure. This rule contrasts TypeScript options against values baked into C++ or the model; to choose between a TypeScript **option** and a TypeScript **constant**, see Principle 6. 6. **Options vs. Constants (bucket by who varies the value)**: - Every parameter belongs in exactly one of three places. Decide by asking _who varies this, and when_: | The value... | Lives as | Example | | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------ | | Varies across the shipped models/variants of the pipeline | A task **option**, set per-model in `models.ts` | Whisper `tiny`/`base`/`small` sizes; normalization factors; labels | | Is fixed by the model architecture or the `.pte` export contract, identical for every shipped variant | A **`const`** in the task file, beside the code that reads it | Static tensor shapes; export-pinned scheduler/decoder scalars | | Is a per-call choice made by the app developer | An **argument** to the worklet executor | `threshold`, `seed`, `prompt` | - **A parameter with exactly one valid value is not an option — it is a constant.** For single-model pipelines (e.g. `sdxsTextToImage`), exposing export-pinned scalars or static shapes as options advertises knobs that either fail schema validation or silently corrupt output when touched. Prefer a `const` with a comment naming _why_ the value is fixed. - **Corollary (a quick smell test):** if every variant in `models.ts` passes an _identical_ options object, those fields are not configuration — move them into the task file as constants and shrink the model type to the paths that actually differ. - Do not keep a loop, parameter, or code path solely because it _looks_ more general. If the surrounding math is only valid for one value (e.g. coefficients pinned to a single distilled timestep), the generality is fake and the parameter is a correctness trap. ## 🚫 Avoid / Anti-Patterns - **Do NOT access tensors by index:** Avoid using `tensors[0]` or `tensors[1]` throughout the function body. Always destructure and name them explicitly. - **Do NOT name options parameters `opts`:** Always name options function parameters `options` (e.g. `options?: { threshold?: number }`). Suffixes like `Opts` for types or properties (e.g. `MyTaskOptions`, `ModelOpts`, `modelOpts`) are acceptable. - **Do NOT define extra inner helper functions:** You must define **exactly two** inner functions inside the `create<Task>` constructor: the `dispose` function and the task `worklet` executor function. **Push back hard against implementing any other helper closures inside the constructor scope.** Placing other helper functions (especially those that are called from inside the worklet and use the `create<Task>` scope variables) inside `create<Task>` creates implicit dependencies and closures that capture variables, making the code extremely difficult to reason about and debug. - **Do NOT throw bare `Error`:** Every failure needs a code. Use `RnExecuTorchError('CODE', msg)`, which works both in the `create<Task>` body and inside the worklet executor. See the [Error Handling Skill](../error-handling/SKILL.md). - **Do NOT leak raw Tensors to consumers:** The returned methods must never return raw `Tensor` objects to the API consumer. Always convert output data to standard JavaScript values/objects before returning. - **Do NOT cross thread boundaries unnecessarily:** Minimize passing heavy objects between JS and the Worklet thread to avoid serialization overhead. - **Do NOT treat the `.pte` model as an unchangeable black box:** Reshape the model's inputs and outputs during the PyTorch export phase to make the mobile client pipeline as lightweight as possible. Do not make input/output contracts so specific that they break extensibility. - **Do NOT expose export-pinned values as options:** If a value is fixed by the `.pte` (static shapes, scalars pinned during export by matching a reference implementation), make it a `const` in the task file. Surfacing it in `models.ts` duplicates it across every variant entry and implies a knob that only ever has one valid value. See Principle 6. --- --- ## 🛠️ Step-by-Step Implementation Template > **Reference:** See [src/extensions/cv/tasks/classification.ts](../../../packages/react-native-executorch/src/extensions/cv/tasks/classification.ts) and [src/hooks/useClassifier.ts](../../../packages/react-native-executorch/src/hooks/useClassifier.ts) for a complete working example of this pattern. ### Step 1: Create the Task File (`src/extensions/<domain>/tasks/<task>.ts`) ```typescript import type { WorkletRuntime } from 'react-native-worklets'; import { tensor } from '../../../core/tensor'; import { loadModel } from '../../../core/model'; import { validateSpec, method, f32 } from '../../../core/schema'; import { wrapAsync } from '../../../core/runtime'; import { type ImageBuffer } from '../image'; import { createImagePreprocessor, type ImagePreprocessorOptions } from '../utils/imagePreprocessor'; export type MyTaskOptions = ImagePreprocessorOptions & { readonly defaultThreshold: number; }; export type MyTaskModel = { readonly modelPath: string; readonly taskOpts: MyTaskOptions; }; export type MyTaskResult = { readonly classId: number; readonly score: number; }; // 1. Helper functions MUST be defined OUTSIDE create<Task> and be worklet-compatible function postprocessOutput(rawData: Float32Array, threshold: number): MyTaskResult[] { 'worklet'; const results: MyTaskResult[] = []; for (let i = 0; i < rawData.length; i++) { if (rawData[i]! > threshold) { results.push({ classId: i, score: rawData[i]! }); } } return results.sort((a, b) => b.score - a.score); } export async function createMyTask( config: MyTaskModel, runtime?: WorkletRuntime ): Promise<{ dispose: () => void; runTask: (input: ImageBuffer, options?: { threshold?: number }) => Promise<MyTaskResult[]>; runTaskWorklet: (input: ImageBuffer, options?: { threshold?: number }) => MyTaskResult[]; }> { const { modelPath, taskOpts } = config; const model = await wrapAsync(loadModel, runtime)(modelPath); // Validate model spec const { variant, dims } = validateSpec(model.schema, { batched: method('forward', [f32(1, 3, 'H', 'W')], [f32(1, 10)]), unbatched: method('forward', [f32(3, 'H', 'W')], [f32(10)]), }); const [H, W] = dims.constant('H', 'W'); const inpShape = { batched: [1, 3, H, W], unbatched: [3, H, W] }[variant]; const outShape = { batched: [1, 10], unbatched: [10] }[variant]; // 2. Pre-allocate static tensors const tensors = [tensor('float32', outShape)] as const; // Idiomatic destructuring and naming with "t" prefix const [tOutput] = tensors; const preprocessor = createImagePreprocessor(taskOpts, inpShape); // 3. Define dispose() immediately after allocation const dispose = () => { preprocessor.dispose(); tensors.forEach((t) => t.dispose()); model.dispose(); }; // 4. Define exactly two inner functions (dispose & runTaskWorklet) const runTaskWorklet = (input: ImageBuffer, options?: { threshold?: number }): MyTaskResult[] => { 'worklet'; // Process input buffer to input tensor const tInput = preprocessor.process(input); model.execute('forward', [tInput], [tOutput]); const data = tOutput.getData(new Float32Array(tOutput.numel)); const threshold = options?.threshold ?? taskOpts.defaultThreshold; // 5. Return standard JS object, never raw Tensor return postprocessOutput(data, threshold); }; const runTask = wrapAsync(runTaskWorklet, runtime); return { runTask, runTaskWorklet, dispose }; } ``` ### Step 2: Create the React Hook Wrapper (`src/hooks/use<Task>.ts`) Wrap the task pipeline in a custom React Hook using the core hooks `useResourceDownload` and `useModel`. This manages downloading, compilation, error tracking, and automatic cleanup of the native memory upon unmounting or config changes. ```typescript import { useModel } from './useModel'; import { useResourceDownload, type ResourceOptions } from './useResourceDownload'; import { createMyTask, type MyTaskModel } from '../extensions/<domain>/tasks/<task>'; export function useMyTask(config: MyTaskModel, options?: ResourceOptions) { // 1. Resolve remote or local asset model path and download progress const { resource, downloadProgress, downloadError } = useResourceDownload(config, options); // 2. Instantiate and compile the task pipeline (with automatic lifecycle cleanup) const { model, error } = useModel(createMyTask, resource); return { isReady: !!model, error: downloadError || error, downloadProgress, resource, runTask: model?.runTask, runTaskWorklet: model?.runTaskWorklet, }; } ``` --- ## 📋 Verification Checklist When adding a task pipeline or React hook, verify that: - [ ] Scratch/output tensors are pre-allocated using `tensor() as const` and prefixed with lowercase `t` (e.g. `tInput`). - [ ] Static tensors are destructured and named (no index-based access in the body). - [ ] The `dispose` function is defined immediately after static allocations. - [ ] Any dynamically allocated tensors are wrapped in `try/finally` and disposed of inside `finally`. - [ ] The constructor contains exactly two inner functions (the `dispose` function and the worklet executor). - [ ] Auxiliary helpers are defined outside the constructor and marked with the `'worklet';` directive if run on the worklet runtime. - [ ] Raw `Tensor` objects are never returned to the consumer. - [ ] Every throw uses `RnExecuTorchError('CODE', message)`. - [ ] Data configurations that genuinely vary across models (e.g. thresholds, labels) are configurable dynamically via the TypeScript task options. - [ ] Every parameter is bucketed per Principle 6: varies across variants → option; fixed by the export → `const` in the task file; per-call choice → executor argument. - [ ] No exposed option has exactly one valid value, and no two `models.ts` variants pass an identical options object.
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub