TensorFlow.js से LiteRT.js पर स्विच करते समय आने वाली तीन चुनौतियाँ
वेब ब्राउज़र में AI मॉडल चलाना रोमांचक तो है, लेकिन जल्द ही इसकी सीमाएं सामने आ जाती हैं। जब हम उच्च-रिज़ॉल्यूशन वाली छवियों को प्रोसेस करते हैं या भारी गणनाएं शुरू करते हैं, तो स्क्रीन लैग करने लगती है। TensorFlow.js (TF.js) WebGL बैकएंड और जावास्क्रिप्ट कर्नल बाइंडिंग का उपयोग करता है, जो बड़े मैट्रिक्स ऑपरेशंस के दौरान गंभीर ओवरहेड पैदा करता है।
दूसरी ओर, LiteRT.js ने C++ नेटिव रनटाइम को WebAssembly (Wasm) में कंपाइल करके ब्राउज़र में पोर्ट किया है। यह संरचना मूल रूप से बाधाओं (bottlenecks) को हल करती है। इसके अलावा, PyTorch मॉडल को वेब पर लाने की प्रक्रिया भी सरल हो जाती है। पहले, PyTorch से ONNX, फिर TensorFlow के माध्यम से TF.js फॉर्मेट में बदलने के कारण ऑपरेटर अनुकूलता (compatibility) खत्म हो जाती थी और सटीकता का नुकसान होता था। अब, केवल एक लाइब्रेरी (ai-edge-torch) के साथ एक मानक .tflite फ़ाइल तैयार करना ही काफी है।
यदि आप अपनी मौजूदा संपत्ति को छोड़ना नहीं चाहते हैं, तो आप Google द्वारा प्रदान किए गए @litertjs/tfjs-interop पैकेज का उपयोग कर सकते हैं। इसमें डेटा प्री-प्रोसेसिंग या पोस्ट-प्रोसेसिंग के लिए TF.js पाइपलाइन को वैसा ही रहने दिया जाता है, और केवल मुख्य मॉडल प्रेडिक्शन निष्पादन भाग को LiteRT.js से बदल दिया जाता है।
NCHW और NHWC के बीच बेमेल को हल करना
PyTorch से परिवर्तित TFLite मॉडल आमतौर पर चैनल-फर्स्ट संरचना NCHW (चैनल, ऊँचाई, चौड़ाई) की मांग करते हैं। हालाँकि, ब्राउज़र Canvas का ImageData सरणी NHWC (ऊँचाई, चौड़ाई, चैनल) फॉर्मेट में होता है, जहाँ पिक्सेल क्रमबद्ध होते हैं। इस अंतर को भरने के लिए हमें मुख्य थ्रेड को लोड किए बिना सिंक्रोनस रूप से प्रोसेस करने वाले प्री-प्रोसेसिंग कोड की आवश्यकता होती है।
सबसे पहले, ImageData के कुल पिक्सेल की संख्या को 3 से गुणा करके एक Float32Array बनाएं। फिर लाल, हरे और नीले प्रत्येक चैनल के लिए फ्लैटनिंग ऑफसेट को 0, कुल पिक्सेल संख्या, और कुल पिक्सेल संख्या का दोगुना निर्धारित करें। अंत में, 0~255 के बीच के पिक्सेल मानों को 255.0 से विभाजित करके सामान्यीकृत (normalize) करें और उन्हें प्रत्येक चैनल ऑफसेट पर असाइन करें। यह इनपुट डेटा को NCHW फ्लैटन्ड टेंसर बफर के रूप में पुनर्व्यवस्थित करने की प्रक्रिया है।
`javascript
/**
- NHWC फॉर्मेट के ImageData बफर को उच्च-प्रदर्शन वाले NCHW Float32Array में तेजी से बदलने के लिए प्री-प्रोसेसिंग यूटिलिटी
- @param {ImageData} imageData - HTML5 Canvas से प्राप्त मूल पिक्सेल डेटा
- @param {number} width - लक्षित मॉडल द्वारा आवश्यक इनपुट छवि की चौड़ाई
- @param {number} height - लक्षित मॉडल द्वारा आवश्यक इनपुट छवि की ऊँचाई
- @returns {Float32Array} NCHW सरणी फॉर्मेट में पुनर्व्यवस्थित फ्लैटन्ड टेंसर बफर
*/
export function preprocessNHWCToNCHW(imageData, width, height) {
const { data } = imageData;
const totalPixels = width * height;
const nchwBuffer = new Float32Array(totalPixels * 3);
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;
}
`
वेब वर्कर और ज़ीरो-कॉपी के साथ स्क्रीन लैग को रोकना
फ्रंटएंड में डीप लर्निंग चलाते समय सबसे भयानक स्थिति तब होती है जब UI रुक जाता है। ब्राउज़र में 60 FPS के स्मूथ एनीमेशन के लिए इवेंट लूप को 16.6ms के भीतर सिंक्रोनस कार्यों को प्रोसेस करना होता है। लेकिन टेंसर ऑपरेशन आसानी से सिंगल-थ्रेडेड मुख्य रेंडरर लूप को ब्लॉक कर देते हैं।
LiteRT.js की बेसिक निष्पादन गति मौजूदा जावास्क्रिप्ट-आधारित टूल की तुलना में लगभग 3 गुना तेज है। यदि आप WebGPU या WebNN एक्सेलेरेशन हार्डवेयर जोड़ते हैं, तो यह CPU मोड की तुलना में कम से कम 5 गुना से लेकर 60 गुना तक तेज हो सकता है। समर्पित NPU का उपयोग करने वाले WebNN बैकएंड के लिए, सिंक्रोनस WebAssembly कर्नल शेड्यूलर को ब्राउज़र के एसिंक्रोनस हार्डवेयर कंट्रोल लूप से जोड़ने के लिए जावास्क्रिप्ट प्रॉमिस इंटीग्रेशन (JSPI) को सक्रिय करना अनिवार्य है। इस एक्सेलेरेशन का उपयोग करते समय मुख्य थ्रेड को बनाए रखने के लिए, लाइब्रेरी इनिशियलाइज़ेशन और संपूर्ण इन्फरेंस पाइपलाइन को वेब वर्कर (Web Worker) के अंदर आइसोलेट करना ही उचित है।
इस दौरान, थ्रेड्स के बीच डेटा संचार में बफर को सीधे पास करने से आंतरिक मेमोरी कॉपी होती है, जिससे CPU और हीप मेमोरी पर ओवरहेड बढ़ता है। ट्रांसफर करने योग्य ऑब्जेक्ट्स (Transferable Objects) का उपयोग करें ताकि भौतिक मेमोरी एड्रेस स्पेस का स्वामित्व (ownership) सीधे ट्रांसफर हो सके और विलंबता (latency) समाप्त हो जाए। स्वामित्व स्थानांतरित होने के बाद बफर ट्रांसफरिंग साइड कॉन्टेक्स्ट में तुरंत अमान्य हो जाता है, जिससे थ्रेड सुरक्षा भी सुनिश्चित होती है।
`javascript
// litert-worker.js - बैकग्राउंड इन्फरेंस ऑपरेशन के लिए समर्पित वेब वर्कर मॉड्यूल
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 - मुख्य थ्रेड के लिए AI ऑर्केस्ट्रेटर क्लास
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]
);
});
}
}
`
स्वचालित गारबेज कलेक्शन पर भरोसा न करें
TF.js का उपयोग करते समय, tf.tidy() के साथ सिंक्रोनस कॉल स्कोप के भीतर टेंसर को हटाना एक मानक पैटर्न था। लेकिन जब एसिंक्रोनस कोड या प्रॉमिस शामिल होते थे, तो वे अक्सर एसिंक्रोनस कार्य समाप्त होने से पहले ही स्कोप से बाहर निकलकर टेंसर को नष्ट कर देते थे या उन्हें हटाना भूल जाते थे, जिससे बग पैदा होते थे।
LiteRT.js और भी अधिक कठोर है। यह ब्राउज़र इंजन के गारबेज कलेक्शन (GC) के अधीन नहीं है। WebAssembly लीनियर वर्चुअल मेमोरी स्पेस और WebGPU बफर की हीप स्थिति को V8 जैसे जावास्क्रिप्ट इंजन ट्रैक नहीं कर सकते। यदि आप कोड समाप्त होने पर टेंसर इंस्टेंस पर स्पष्ट रूप से .delete() कॉल नहीं करते हैं, तो ब्राउज़र की मेमोरी असीमित रूप से बढ़ती जाएगी। यदि आप ऐसी सेवा बना रहे हैं जो हाई-डेफिनिशन वीडियो फ्रेम को प्रति सेकंड कई बार स्ट्रीम करती है, तो टैब कुछ ही मिनटों में क्रैश हो जाएगा।
मन की शांति के लिए, आपको एक स्कोप ट्रैकर क्लास बनानी चाहिए जो एसिंक्रोनस पाइपलाइन में उत्पन्न सभी टेंसरों के जीवनकाल को रिकॉर्ड करे और उनके सामूहिक विनाश (disposal) की गारंटी दे।
`javascript
/**
- WebAssembly हीप टेंसरों को मैन्युअल रूप से ट्रैक करने और निश्चित रूप से नष्ट करने के लिए एसिंक्रोनस मेमोरी स्कोप मैनेजर
*/
export class LiteRtScopeTracker {
constructor() {
this.trackList = new Set();
}
/**
- बनाए गए या स्थानांतरित किए गए टेंसर को लाइफसाइकिल मैनेजमेंट लिस्ट में शामिल करें
- @param {Tensor} tensor - ट्रैकिंग और विनाश के लिए LiteRT.js टेंसर
- @returns {Tensor} इनलाइन कोड लेखन का समर्थन करने के लिए टेंसर ऑब्जेक्ट वापस करें
*/
register(tensor) {
if (tensor && typeof tensor.delete === 'function') {
this.trackList.add(tensor);
}
return tensor;
}
/**
- एसिंक्रोनस निष्पादन ब्लॉक के भीतर सुरक्षित टेंसर पाइपलाइन प्रबंधन संरचना को लागू करें
- @param {Function} asyncCallable - एसिंक्रोनस इन्फरेंस बिजनेस लॉजिक फंक्शन
- @returns {Promise<*>} निष्पादन ब्लॉक द्वारा लौटाया गया अंतिम परिणाम
*/
async enforceScope(asyncCallable) {
try {
const outputResult = await asyncCallable(this);
if (Array.isArray(outputResult)) {
outputResult.forEach((item) => this.trackList.delete(item));
} else {
this.trackList.delete(outputResult);
}
return outputResult;
} finally {
this.disposeAll();
}
}
/**
- प्रबंधन के तहत बंधे सभी जीवित नेटिव TFLite टेंसर को Wasm क्षेत्र से स्थायी रूप से हटा दें
*/
disposeAll() {
for (const tensor of this.trackList) {
try {
tensor.delete();
} catch (err) {
console.error('नेटिव Wasm टेंसर मेमोरी को साफ करते समय एक त्रुटि हुई:', err);
}
}
this.trackList.clear();
}
}
`
`javascript
// मेमोरी स्कोप ट्रैकर का उपयोग करके सुरक्षित और मजबूत मल्टी-एसिंक्रोनस AI इन्फरेंस कार्यान्वयन का उदाहरण
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('मॉडल पाइपलाइन निष्पादन के दौरान घातक क्रैश हुआ:', err);
throw err;
}
}
`
भारी Wasm फ़ाइलों को सशर्त रूप से लोड करना
संसाधन डाउनलोड आकार बढ़ने की समस्या को भी नजरअंदाज नहीं किया जा सकता है। बिल्ड बंडलर में ट्री-शेकिंग को प्रभावी बनाने के लिए, CommonJS-स्टाइल के स्टेटिक रेफरेंस को हटाएं और ES6 मॉड्यूल सिंटैक्स (import/export) पर आधारित सोर्स कोड लिखें। बंडल को हल्का रखने के लिए बिल्ड टूल की sideEffects: false सेटिंग का भी ध्यान रखें।
LiteRT.js कोर रनटाइम @litertjs/core डिवाइस के प्रदर्शन के आधार पर तीन अलग-अलग WebAssembly कर्नल बिल्ड को चुनकर लोड करता है। Chrome या Edge जैसे आधुनिक ब्राउज़र में, यह मल्टी-थ्रेडिंग और SIMD का समर्थन करने वाले मॉड्यूल (litert_wasm_simd.wasm) का चयन करता है, और Safari जैसे लेगेसी वातावरण में डिफ़ॉल्ट फॉलबैक मॉड्यूल (litert_wasm.wasm) का उपयोग करता है। यदि GPU संकलन विफल हो जाता है, तो XNNPACK रनटाइम एक सहायक के रूप में कार्य करता है जो हार्डवेयर ऑपरेटरों को CPU Wasm सैंडबॉक्स में धकेल देता है।
शुरुआती लोडिंग में देरी से बचने के लिए, आपको एक्सेलेरेटर की क्षमता की जांच करके मॉड्यूल को गतिशील रूप से लोड करना चाहिए।
`javascript
// litert-loader.js - रनटाइम डिवाइस डिटेक्शन और एक्सेलेरेटर डायनामिक बाइंडिंग इंजन
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 प्रोफ़ाइल जांच विफल रही, निष्पादन श्रृंखला को फॉलबैक 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
};
}
`
वेब वर्कर के माध्यम से मेमोरी ओनरशिप ट्रांसफर और Wasm क्षेत्र के ऑब्जेक्ट्स को स्पष्ट रूप से नष्ट करने को अपने डिज़ाइन आधार में रखें। जब आप डेटा प्रवाह को सीधे नियंत्रित करना शुरू करते हैं, तो आप बिना किसी ब्राउज़र क्रैश की चिंता किए प्रोडक्शन में ऑन-डिवाइस AI सेवाओं को तैनात कर सकते हैं।