ESP32S3 और 16MB फ्लैश के साथ वास्तविक समय इनपुट प्राप्त करने वाला ऑन-डिवाइस LLM बनाना
जब आप $8 के ESP32-S3 बोर्ड पर 28.9 मिलियन पैरामीटर वाला भाषा मॉडल चलाने का मन बनाते हैं, तो आप जल्दी ही खुद को लाचार महसूस करते हैं। डेमो वीडियो में बेहतरीन तरीके से चलने वाला मॉडल जैसे ही आपके द्वारा बनाए गए हार्डवेयर पेरिफेरल्स से मिलता है, वैसे ही हैंग हो जाता है। SRAM केवल 512KB है, और हर बार जब आप एक वाक्य बदलते हैं, तो आपको 15MB का नया पार्टीशन फ्लैश करना पड़ता है, जो विकास के दौरान गुस्सा दिलाता है। इन बाधाओं को पार करने और वास्तविक कीपैड इनपुट के साथ काम करने वाला डिवाइस बनाने के लिए, आपको मेमोरी Caching और I/O बफर्स को शुरू से अलग तरह से डिज़ाइन करना होगा।
हर बार रिफ्लैश किए बिना सीरियल के माध्यम से प्रॉम्प्ट डालना
केवल एक परीक्षण वाक्य को बदलने के लिए बोर्ड को फिर से फ्लैश करना समय की बर्बादी है। यदि आप असमकालिक (asynchronous) UART इंटरप्ट और FreeRTOS सेमफ़ोर (semaphores) को एक साथ बांधते हैं, तो बोर्ड को रीबूट करने की कोई आवश्यकता नहीं है। मॉडल आपके द्वारा सीरियल मॉनिटर या कीपैड पर टाइप किए जाने वाले प्रत्येक वाक्य को तुरंत समझ लेता है.
- GPIO 16/17 पिन को UART_NUM_2 पोर्ट के रूप में 지정 करें, हार्डवेयर FIFO थ्रेशोल्ड को 120 बाइट्स पर सेट करें, और रिसीव इंटरप्ट खोलें।
- बिल्ट-इन SRAM में 2048-बाइट रिंग बफर बनाएं। यह अनुमान (inference) गणना के दौरान डेटा ओवरफ्लो होने और क्रैश होने से रोकता है।
- न्यू लाइन वर्णों का पता लगाने वाले स्ट्रीमिंग पार्सर को संलग्न करें, और 43,056-बाइट BTK1 एनकोडर लाइब्रेरी का उपयोग करके इनपुट टेक्स्ट को सीधे टोकन आईडी सरणी में परिवर्तित करें।
- xPromptSemaphore डुअल सेमफ़ोर सेट करें। जब रिसीव टास्क टोकन लिखना समाप्त कर देता है और xSemaphoreGive को कॉल करता है, तो प्रतीक्षा कर रहा इन्फेरेंस टास्क xSemaphoreTake के माध्यम से मेमोरी लॉक प्राप्त करता है और गणना शुरू करता है.
इस कॉन्फ़िगरेशन के साथ, आप प्रॉम्प्ट संशोधन और परीक्षण में लगने वाले समय को 80% से अधिक कम कर सकते हैं।
`
+------------------+ +-------------------+ +--------------------+
| External UART | ---> | HW FIFO Buffer | ---> | SW Ring Buffer |
| (Keypad/Monitor) | | (120 Bytes) | | (2048 Bytes) |
+------------------+ +-------------------+ +--------------------+
|
v
+------------------+ +-------------------+ +--------------------+
| LLM Forward Pass | <--- | xPromptSemaphore | <--- | BTK1 Tokenizer |
| (Inference Task) | | (Binary Lock) | | (43,056 B Library) |
+------------------+ +-------------------+ +--------------------+
`
प्रति-परत एम्बेडिंग विभाजन (Per-Layer Embeddings) और SRAM Caching के साथ 14 टोकन गति प्राप्त करना
Google Gemma 3n शोधकर्ताओं द्वारा प्रस्तावित Per-Layer Embeddings (PLE) संरचना का उपयोग करके, आप 4-बिट तक कम किए गए 14.9MB मॉडल को विभाजित और लोड कर सकते हैं। 25 मिलियन मापदंडों तक के एम्बेडिंग टेबल को 16MB SPI फ्लैश में रखा गया है, और आउटपुट हेड वेट (3.1 मिलियन पैरामीटर) और KV कैश को 8MB PSRAM में रखा गया है। केवल सबसे अधिक उपयोग किए जाने वाले 559K पैरामीटर डेंस कंप्यूट कोर और हॉट एक्टिवेशन बफर को 512KB SRAM में एम्बेड किया गया है।
गति को 14 टोकन प्रति सेकंड से अधिक तक बढ़ाना अपेक्षा से अधिक कठिन है।
__attribute__((noinline)) विशेषता को सीधे matvec_i8_range गणना फ़ंक्शन में जोड़ें। यदि कंपाइलर अपने आप इनलाइनिंग करता है, तो I-RAM कैश मिस हो जाएगा और गणना का समय 94.9ms से बढ़कर 155.2ms हो जाएगा।
ple_model_proj गणना और qkv गणना को ESP32-S3 के Dual Xtensa LX7 कोरों के बीच विभाजित करें। वांछित गति प्राप्त करने के लिए आपको डुअल कोरों को एक साथ चलाना होगा।
- फ्लैश मेमोरी बॉटलनैक को दूर करने के लिए प्रीफ़ेच बफर को चौड़ा करें, जो SRAM की शेष खाली जगह में प्रारंभिक परत एम्बेडिंग डेटा को पहले से लोड करता है।
जब केवल C भाषा में पोर्ट किया जाता है, तो गति 0.57 टोकन प्रति सेकंड की निराशाजनक गति देती है। हालाँकि, INT8 स्टेजिंग और SRAM प्रीफ़ेच समाप्त होने के बाद, यह 14.0 टोकन प्रति सेकंड से अधिक हो जाता है।
| अनुकूलन चरण |
प्रति टोकन गणना विलंबता |
प्रति सेकंड टोकन जनरेशन |
मुख्य रूप से प्रयुक्त तकनीक |
| C शुद्ध पोर्टिंग (Baseline) |
1,757.2 ms |
0.57 tok/s |
सिंगल कोर, FP32 गणना |
| PSRAM Head & स्केलर अनुकूलन |
193.9 ms |
4.61 tok/s |
आउटपुट हेड PSRAM प्लेसमेंट |
| डुअल कोर FP32 अनुप्रयोग |
139.4 ms |
6.22 tok/s |
डुअल कोर लेयर स्प्लिट गणना |
| INT8 Staging + SRAM अनुकूलन |
94.9 ms |
9.88 tok/s |
INT8 परिमाणीकरण, noinline अनुप्रयोग |
| SRAM प्रीफ़ेच बफर विस्तार |
~71.4 ms |
14.0+ tok/s |
प्रारंभिक लेयर SRAM प्रीफ़ेच |
210mA करंट को नियंत्रित करने के लिए लाइट स्लीप कंट्रोल
240MHz क्लॉक पर डुअल कोरों को लगातार चलाने से खपत करंट 210mA (777mW) तक बढ़ जाता है। बोर्ड के गर्म होने की बात तो दूर, बैटरी बहुत जल्दी खत्म हो जाती है। जब कोई इन्फेरेंस नहीं हो रहा हो, तो esp_pm_configure कमांड का उपयोग करके क्लॉक को 40MHz तक कम किया जाना चाहिए और स्वचालित लाइट स्लीप को सक्षम किया जाना चाहिए।
esp_pm_config_esp32s3_t संरचना में, max_freq_mhz को 240, min_freq_mhz को 40 पर सेट करें, और light_sleep_enable को true पर चालू करें।
esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_ON) निष्पादित करें। स्लीप अवस्था में प्रवेश करते समय भी डेटा को नष्ट होने से बचाने के लिए SPI फ्लैश और RAM पावर बनाए रखी जानी चाहिए।
- सीरियल सिग्नल आने पर CPU को स्लीप से जगाने के लिए रिकवरी इंटरप्ट ट्रिगर करने के लिए
uart_set_wakeup_threshold और esp_sleep_enable_uart_wakeup का उपयोग करें।
यदि आप 3.7V 1000mAh लिथियम-पॉलिमर बैटरी के साथ लगातार गणना करते हैं, तो यह केवल 4.7 घंटे तक चलेगी। हालाँकि, लाइट स्लीप स्टैंडबाय मोड में, करंट तेजी से गिरकर 0.24mA (0.88mW) हो जाता है। वास्तविक उपयोग वातावरण में जहाँ स्टैंडबाय समय मिलाया जाता है, औसत करंट 15 ~ 30mA स्तर पर बनाए रखा जाता है, जिससे बैटरी का उपयोग जीवन 3 गुना से अधिक बढ़ जाता है।