Tres campos de minas a sortear al pasar de TensorFlow.js a LiteRT.js
TuBrief 편집팀
2026년 7월 18일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Ejecutar modelos de IA en el navegador web es emocionante, pero pronto te topas con limitaciones. Cuando procesas imágenes de alta resolución o inicias cálculos pesados, la pantalla comienza a sufrir tirones. TensorFlow.js (TF.js) utiliza el backend de WebGL y el binding de núcleos de JavaScript, lo cual genera una sobrecarga significativa en operaciones matriciales a gran escala.
Por el contrario, LiteRT.js trasplanta el runtime nativo de C++ al navegador mediante la compilación a WebAssembly (Wasm). Es una estructura que resuelve los cuellos de botella de raíz. Además, el proceso de llevar modelos de PyTorch a la web se simplifica. Antes, el flujo de PyTorch a ONNX, pasando por TensorFlow para llegar al formato TF.js, provocaba la ruptura de la compatibilidad de operadores y la pérdida de precisión. Ahora, con una sola librería (ai-edge-torch), basta con exportar un archivo estándar .tflite y listo.
Si no quieres desechar tus recursos existentes, puedes usar el paquete @litertjs/tfjs-interop proporcionado por Google. El enfoque consiste en mantener el preprocesamiento y postprocesamiento de datos en el pipeline de TF.js, sustituyendo únicamente la parte de ejecución de inferencia del modelo por LiteRT.js.
Los modelos TFLite convertidos desde PyTorch suelen requerir una estructura de canales primero (: canales, alto, ancho). Sin embargo, el array ImageData de un Canvas en el navegador sigue el formato (alto, ancho, canales), donde los píxeles están dispuestos de forma contigua. Se necesita un código de preprocesamiento que resuelva esta diferencia de forma síncrona y sin sobrecargar el hilo principal.
Primero, creamos un Float32Array multiplicando el número total de píxeles de ImageData por 3. Luego, definimos las posiciones de desplazamiento (offset) de aplanamiento para los canales rojo, verde y azul en 0, el total de píxeles y el doble del total de píxeles, respectivamente. Finalmente, normalizamos los valores de los píxeles de 0 a 255 dividiéndolos por 255.0 y los asignamos a cada desplazamiento de canal. Este es el proceso de reorganización de los datos de entrada en un búfer de tensores aplanados .
`javascript
/**
const rChannelOffset = 0;
const gChannelOffset = totalPixels;
const bChannelOffset = totalPixels * 2;
for (let i = 0; i < totalPixels; i++) {
const srcIndex = i * 4;
nchwBuffer[rChannelOffset + i] = data[srcIndex] / 255.0;
nchwBuffer[gChannelOffset + i] = data[srcIndex + 1] / 255.0;
nchwBuffer[bChannelOffset + i] = data[srcIndex + 2] / 255.0;
}
return nchwBuffer;
}
`
Lo peor que puede pasar al ejecutar aprendizaje profundo en el frontend es que la interfaz se congele. Para que el navegador muestre animaciones fluidas a 60 FPS, el bucle de eventos debe procesar las tareas síncronas en menos de 16.6 ms. Sin embargo, las operaciones con tensores bloquean fácilmente el bucle del renderizador principal, que es monohilo.
LiteRT.js es aproximadamente tres veces más rápido en su ejecución base que las herramientas basadas en JavaScript tradicional. Al añadir aceleración mediante WebGPU o WebNN, la velocidad aumenta de 5 a 60 veces en comparación con el modo CPU. El backend de WebNN, que utiliza una NPU dedicada, requiere activar obligatoriamente la integración de promesas de JavaScript (JSPI) para conectar el planificador de núcleos de WebAssembly síncronos con el bucle de control de hardware asíncrono del navegador. Para aprovechar esta aceleración manteniendo el hilo principal activo, la inicialización de la librería y todo el pipeline de inferencia deben aislarse dentro de un Web Worker.
En este punto, si simplemente pasas el búfer en la comunicación entre hilos, se produce una copia interna de memoria, lo que genera sobrecarga en la CPU y la memoria heap. Es necesario utilizar Transferable Objects para transferir la propiedad misma de la dirección de memoria física y eliminar la latencia. Una vez transferida la propiedad, el búfer queda invalidado inmediatamente en el contexto emisor, garantizando así la seguridad entre hilos.
`javascript
// litert-worker.js - Módulo de Web Worker exclusivo para operaciones de inferencia en segundo plano
import { loadLiteRt, loadAndCompile, Tensor } from '@litertjs/core';
let compiledModel = null;
let isLoaded = false;
self.onmessage = async (event) => {
const { type, payload } = event.data;
switch (type) {
case 'LOAD_MODEL':
try {
await loadLiteRt(payload.wasmDirectory, { jspi: payload.enableJspi || false });
compiledModel = await loadAndCompile(payload.modelUrl, {
accelerator: payload.accelerator || 'webgpu'
});
isLoaded = true;
self.postMessage({ type: 'MODEL_READY' });
} catch (err) {
self.postMessage({ type: 'ERROR', error: Initialization failed: ${err.message} });
}
break;
case 'RUN_INFERENCE':
if (!isLoaded || !compiledModel) {
self.postMessage({ type: 'ERROR', error: 'Model has not been loaded' });
return;
}
try {
const rawInputData = payload.bufferData;
const inputShape = payload.shape;
const inputTensor = new Tensor(rawInputData, inputShape);
const results = await compiledModel.run(inputTensor);
const cpuOutputTensor = await results[0].moveTo('wasm');
const outputBuffer = cpuOutputTensor.toTypedArray();
inputTensor.delete();
cpuOutputTensor.delete();
results[0].delete();
self.postMessage(
{
type: 'INFERENCE_COMPLETE',
payload: {
data: outputBuffer,
shape: results[0].shape
}
},
[outputBuffer.buffer]
);
} catch (err) {
self.postMessage({ type: 'ERROR', error: `Inference failed: ${err.message}` });
}
break;
default:
self.postMessage({ type: 'UNKNOWN_OP' });
}
};
`
`javascript
// litert-bridge.js - Clase orquestadora de IA para el hilo principal
export class LiteRtBridge {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.promiseMap = new Map();
this.tokenCounter = 0;
this.worker.onmessage = (event) => {
const { type, payload, error } = event.data;
if (type === 'MODEL_READY') {
if (this.initResolve) this.initResolve();
} else if (type === 'INFERENCE_COMPLETE') {
const currentToken = this.tokenCounter;
const promiseHandler = this.promiseMap.get(currentToken);
if (promiseHandler) {
promiseHandler.resolve(payload);
this.promiseMap.delete(currentToken);
}
} else if (type === 'ERROR') {
const currentToken = this.tokenCounter;
const promiseHandler = this.promiseMap.get(currentToken);
if (promiseHandler) {
promiseHandler.reject(new Error(error));
this.promiseMap.delete(currentToken);
} else if (this.initReject) {
this.initReject(new Error(error));
}
}
};
}
bootstrap(wasmDirectory, modelUrl, accelerator = 'webgpu') {
return new Promise((resolve, reject) => {
this.initResolve = resolve;
this.initReject = reject;
this.worker.postMessage({
type: 'LOAD_MODEL',
payload: { wasmDirectory, modelUrl, accelerator, enableJspi: true }
});
});
}
execute(inputFloat32Array, inputShape) {
return new Promise((resolve, reject) => {
this.tokenCounter++;
this.promiseMap.set(this.tokenCounter, { resolve, reject });
this.worker.postMessage(
{
type: 'RUN_INFERENCE',
payload: {
bufferData: inputFloat32Array,
shape: inputShape
}
},
[inputFloat32Array.buffer]
);
});
}
}
`
Al usar TF.js, el patrón estándar era eliminar los tensores dentro de un scope de llamada síncrona mediante tf.tidy(). Sin embargo, cuando se entrelazan códigos asíncronos o promesas, esto provocaba errores, como liberar tensores antes de que terminara la tarea asíncrona o dejar de liberar recursos (memory leaks).
LiteRT.js es más implacable; no está sujeto a la recolección de basura (GC) del motor del navegador. El motor de JavaScript, como V8, no puede rastrear el estado del heap del espacio de memoria virtual lineal de WebAssembly ni de los búferes de WebGPU. Si no llamas explícitamente a .delete() en las instancias de tensores que ya no se usan, la memoria del navegador crecerá infinitamente. En un servicio que transmite fotogramas de video de alta calidad docenas de veces por segundo, la pestaña morirá en cuestión de minutos.
Para trabajar con tranquilidad, debes crear una clase rastreadora de scope que registre el tiempo de vida de los tensores generados en todo el pipeline asíncrono y garantice su destrucción colectiva.
`javascript
/**
/**
/**
/**
`
`javascript
// Ejemplo de implementación de procesamiento de inferencia de IA asíncrona múltiple, segura y robusta utilizando el rastreador de ámbito de memoria
export async function runRobustVisionInference(rawPixelArray, compiledModel) {
const scopeTracker = new LiteRtScopeTracker();
try {
return await scopeTracker.enforceScope(async (scope) => {
const inputTensor = scope.register(new Tensor(rawPixelArray, [1, 3, 224, 224]));
const predictionResults = await compiledModel.run(inputTensor);
predictionResults.forEach((tensor) => scope.register(tensor));
const firstOutputTensor = predictionResults[0];
const wasmTransferTensor = scope.register(await firstOutputTensor.moveTo('wasm'));
const targetJsArray = wasmTransferTensor.toTypedArray();
return targetJsArray;
});
} catch (err) {
console.error('Fatal crash occurred during the model pipeline execution:', err);
throw err;
}
}
`
El problema del aumento del tamaño de la descarga de recursos tampoco debe ignorarse. Para que el tree-shaking funcione en el bundler de compilación, se debe eliminar la referencia estática al estilo CommonJS y escribir el código fuente basado en la sintaxis de módulos ES6 (import/export). También hay que asegurarse de incluir la configuración sideEffects: false de la herramienta de construcción para reducir el paquete final.
El runtime principal de LiteRT.js, @litertjs/core, selecciona dinámicamente tres builds de núcleos de WebAssembly según el rendimiento del dispositivo. En navegadores modernos como Chrome o Edge, se carga el módulo compatible con multihilo y SIMD (litert_wasm_simd.wasm), mientras que en entornos heredados como Safari, se utiliza el módulo de respaldo predeterminado (litert_wasm.wasm). Si la compilación de GPU falla, el runtime de XNNPACK entra como auxiliar para trasladar todos los operadores de hardware a una zona de pruebas (sandbox) de Wasm en la CPU.
Para evitar retrasos en la carga inicial, es fundamental verificar el acelerador según las especificaciones y cargar el módulo de forma dinámica.
`javascript
// litert-loader.js - Motor de exploración de dispositivos en tiempo de ejecución y acoplamiento dinámico de aceleradores
export async function bootstrapHighPerformanceInferenceEngine() {
const supportsWebGpu = 'gpu' in navigator;
let chosenAccelerator = 'wasm';
if (supportsWebGpu) {
try {
const gpuAdapter = await navigator.gpu.requestAdapter();
if (gpuAdapter) {
const info = await gpuAdapter.requestDevice();
if (info) {
chosenAccelerator = 'webgpu';
}
}
} catch (e) {
console.warn("GPU profile probe failed, resolving execution chain to fallback WASM.");
}
}
const { loadLiteRt, loadAndCompile } = await import('@litertjs/core');
const cdnWasmHostPath = 'https://cdn.jsdelivr.net/npm/@litertjs/core/wasm/';
await loadLiteRt(cdnWasmHostPath, {
jspi: chosenAccelerator === 'webnn'
});
return {
loadAndCompile,
chosenAccelerator
};
}
`
Establece como base de diseño la transferencia de propiedad de memoria a través de Web Workers y la destrucción explícita de objetos en el área Wasm. Una vez que empieces a controlar directamente el flujo de datos, podrás poner en producción servicios de IA en el dispositivo (on-device AI) sin temor a que el navegador explote.