视觉与机电作品 / 小模型训练与执行验收方法发布 内容更新

ESP32 小模型部署实验:合成数据、PTQ与目标自检

完整训练圆方小模型、隔开校准与测试并核模型身份;桌面量化通过,S3 QEMU参考失败如实保留,目标正确性和实板资源待验证。

读完能做什么
能从数据身份追踪到模型与固件,在目标参考失败时停止,并记录完整资源与诊断证据。
开始之前
完成V03固定输入推理与E07组件锁,能读张量dtype/形状/指数,保存同次模型、配置、ELF和原log;刷前按E09备份。
所需器材
ESP32-S3-EYE-MB_V2.2 / 8MiB Flash + 8MiB OPI PSRAM,可靠Micro-USB数据线和电脑USB供电、IDF5.5.5;独立Python3.12训练venv、固定正常工具包、原Flash/布局完整备份;不接相机应用、外部器件或执行器
版本组合
固件:IDF5.5.5 b774170… / ESP-DL3.3.8 77a8a624… / dl_fft0.8.0训练转换:Python3.12.13 / torch2.8.0 / ESP-PPQ1.3.11 e8881cb9… / CPU单线程任务与模型:合成16×16灰度;922参数 / 7296B / int8 NHWC输入指数−7结果边界:CPU浮点/PTQ test各128/128;QEMU参考FAILED,目标/实体待验证
适用范围

仅S3-EYE MB_V2.2 / DIO80+OPI80 / 原生USB19/20;合成圆方二分类,无实板或相机正确性;QEMU参考FAILED、0CASE,保留STOP。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/vision-custom-model-sources.json:固定DL3.3.8/PPQ/量化与模型身份。验证日期:
构建 / 配置已通过
docs/esp32-evidence/vision-custom-model-builds.json:原8通道/7296B模型与修后工具的native/UART各真实Exit0,800×2预输入/32保留产品,非目标执行通过。对象:esp32s3 / S3-EYE MB_V2.2版本:IDF5.5.5 / ESP-DL3.3.8验证日期:
仿真未通过
docs/esp32-evidence/vision-custom-model-qemu.json:参考28/模拟−128,0CASE;层探针与16通道候选仍失败,根因未定。桌面CPU/PTQ另有通过证据。对象:official QEMU9.2.2 / ESP32-S3 / 8MiB OPI PSRAM验证日期:
本站实板待验证
无实体推理、相机泛化、时延/heap/电气/能耗记录;物理CSV仅表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 小模型训练与执行验收方法

先把任务缩到能审计的一份小模型

这份完整工程从生成16×16灰度圆/方轮廓,到40轮训练、标准PTQ、128个合成留出样本,再到S3构建和模型执行自检。circle=0、square=1,不识别现实照片,也不初始化相机、LCD、SD、麦克风、无线或执行器。

本站实际执行了桌面训练/PTQ与指定SDK构建;补充S3 QEMU参考自检失败,目标推理正确性尚未证明。本文提供完整实验与诊断方法,不能把CPU留出集100%写成实板或相机准确率。实体板只有先通过model->test,才可继续128样本;所有物理表仍空。

参考:[1] ESP-DL3.3.8固定manifest与IDF范围[2] 固定Model API:test、run与输入输出[3] 固定模型执行/参考比较实现[6] ESP-PPQ1.3.11固定训练后量化入口与目标

02 / 小模型训练与执行验收方法

固定板、PSRAM与运行库,不混用Tensor接口

仅ESP32-S3-EYE-MB_V2.2,8MiB Flash/DIO80、8MiB OPI PSRAM80、CPU240MHz、main栈16KiB,原生Micro-USB Serial/JTAG19/20;无USB-UART桥。只电脑USB供电,不接电池或外设,断电检查板修订和排线。

IDF5.5.5/b774170…,ESP-DL3.3.8/77a8a624…、dl_fft0.8.0及原dependencies.lock。3.3.8指数是ExponentInfo,必须get(),并检查per-channel;不能将C++对象直接传printf变参。启动检查PSRAM已初始化且容量8MiB;之前关闭PSRAM的探索固件在QEMU模型构造处StoreProhibited,已保留,不能因为模型小就认定预编译分配路径无需PSRAM。

代码 / 配置
# ---- main/idf_component.yml ----
dependencies:
  idf: "==5.5.5"
  espressif/esp-dl: "==3.3.8"
  espressif/dl_fft: "==0.8.0"

参考:[1] ESP-DL3.3.8固定manifest与IDF范围[2] 固定Model API:test、run与输入输出[4] 固定TensorBase:ExponentInfo及内存能力[8] 固定S3-EYE指南:MB修订和原生USB

03 / 小模型训练与执行验收方法

数据分割:测试集不参与校准或选模型

生成512训练、128验证、128测试,类别各半;每个样本有固定独立种子,全部768像素SHA互异。位置、半径、线宽、背景和噪声不按标签设置。合成分布内部留出不代表现实分布覆盖。dataset-manifest.json同时保文件与像素SHA。

训练40轮,以验证accuracy优先、再按验证loss选checkpoint;最后才评估test。64个PTQ校准ID严格从train选,calibration-ids.json可逐项核对。test-0000仅作为导出器的参考输入/输出,不是校准样本;不能拿测试预测改阈值再称独立测试。

数据数量与唯一用途
train512:梯度训练,64个校准样本只从这里选
validation128:40轮中选择checkpoint
test128:最后评估并全部保留预测;test-0000另作导出参考

参考:[6] ESP-PPQ1.3.11固定训练后量化入口与目标[7] ESP-DL固定支持算子与量化规范

04 / 小模型训练与执行验收方法

922参数网络与int8输入映射

网络为Conv1→8/ReLU/AvgPool2/Conv8→8/ReLU/AvgPool2/Flatten/Linear128→2,共922参数。导出图含Conv、AveragePool、Transpose、Flatten、Gemm与融合激活,须核固定目标算子和形状;PyTorch能跑不等于S3已正确执行。

ESP-PPQ1.3.11采用S3的8bit对称、2幂指数与ROUND_HALF_UP。训练浮点输入pixel/255;导出为INT8 NHWC1×16×16×1、exponent−7,固件用整数映射四舍五入并把128饱和到127。直接uint8转int8会把高像素绕成负数。tests/pixels_host.c直接包含生产头,检查全部256输入。

代码 / 配置
// ---- main/pixels.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
/* Fixed exported input exponent -7: real=pixel/255, q=round_half_up(real*128).
 * Inputs are nonnegative. Saturate128 to127, never wrap into a negative int8. */
static inline int8_t v05_quantize_pixel(uint8_t pixel) {
    unsigned q=((unsigned)pixel*128U+127U)/255U;
    return(int8_t)(q>127U?127U:q);
}

参考:[4] 固定TensorBase:ExponentInfo及内存能力[6] ESP-PPQ1.3.11固定训练后量化入口与目标[7] ESP-DL固定支持算子与量化规范

05 / 小模型训练与执行验收方法

真实桌面结果与可获取的全部数据

本轮CPU浮点及CPU量化图test矩阵都为行truth/列prediction [[64,0],[0,64]],128条prediction改变0。所有logits、标签和预测都保留,不只给成功截图;这是有限合成留出集,未做置信度校准或域外拒识。

下载包含768PNG、manifest、64校准ID、40轮history、浮点权重、ONNX、转换配置/信息及128预测;当前模型7296B、SHA256 c495b5de7df02046c6596ae84d5c7c832e88c45a0657165fe1fb3eef7da594ec。另一个16通道诊断候选有自己的数据和产物,不替换本下载的原8通道模型。

参考:[6] ESP-PPQ1.3.11固定训练后量化入口与目标[7] ESP-DL固定支持算子与量化规范

06 / 小模型训练与执行验收方法

先构建固定输入,保存同次可恢复档案

解压后激活IDF5.5.5,在工程根使用新目录:IDF_PY_BUILD_JOBS=2 idf.py -B build-v05 -D IDF_TARGET=esp32s3 -D SDKCONFIG=build-v05/sdkconfig build。三个输入逐SHA门禁同时核模型、128×256像素和128标签;不删锁或跳过门禁。模型已在app RODATA内,资源汇总不要再重复加7296B。

NVS0x9000/0x6000、phy0xf000/0x1000、factory0x10000/0x300000,无OTA。保存源码、锁、实际配置、compile_commands、完整log、ELF/map、app/bootloader/分区bin与flasher_args。刷前按E09私密备份原板完整8MiB Flash/布局/NVS和SHA,不能自动erase或把app当完整Flash写0x0。

代码 / 配置
# ---- main/CMakeLists.txt ----
include(${CMAKE_CURRENT_LIST_DIR}/inputs.cmake)
foreach(item shape-s3.espdl heldout-pixels.bin heldout-labels.bin)
    file(SHA256 "${CMAKE_CURRENT_LIST_DIR}/${item}" actual_sha)
    if(NOT actual_sha STREQUAL "${V05_SHA_${item}}")
        message(FATAL_ERROR "V05 input identity mismatch: ${item}")
    endif()
endforeach()
idf_component_register(SRCS "main.cpp" INCLUDE_DIRS "." REQUIRES esp-dl esp_timer heap esp_psram
    EMBED_FILES "shape-s3.espdl" "heldout-pixels.bin" "heldout-labels.bin")
target_compile_definitions(${COMPONENT_LIB} PRIVATE V05_MODEL_SHA256="${V05_SHA_shape-s3.espdl}")

参考:[1] ESP-DL3.3.8固定manifest与IDF范围[2] 固定Model API:test、run与输入输出[8] 固定S3-EYE指南:MB修订和原生USB

07 / 小模型训练与执行验收方法

参考自检先于128样本,失败就停止

主程序先查二进制长度、PSRAM容量,再构造模型并记录LOAD。model->test对标准导出参考进行目标执行比较;失败会释放模型、打印STOP并退出,零条CASE不能写作128次成功。之后才核INT8输入/输出、NHWC形状、输入指数−7与输出两项,并拒绝per-channel输入/输出。

每条CASE打印test-ID、truth/pred、两项raw logit、共同exponent和围model->run的us;打印发生在计时结束后。等值固定选circle只是tie规则,未做softmax,raw logit或差值不是校准概率。MEM分别列internal/PSRAM的free与largest;free不等于最大连续块,单轮released近似恢复也不证明长期无泄漏。

代码 / 配置
// ---- main/main.cpp ----
/* SPDX-License-Identifier: MIT */
#include <cstdio>
#include <cinttypes>
#include <cstring>
#include "dl_model_base.hpp"
#include "esp_heap_caps.h"
#include "esp_timer.h"
#include "esp_system.h"
#include "esp_psram.h"
#include "pixels.h"
extern const uint8_t model_start[] asm("_binary_shape_s3_espdl_start");
extern const uint8_t pixels_start[] asm("_binary_heldout_pixels_bin_start");
extern const uint8_t pixels_end[] asm("_binary_heldout_pixels_bin_end");
extern const uint8_t labels_start[] asm("_binary_heldout_labels_bin_start");
extern const uint8_t labels_end[] asm("_binary_heldout_labels_bin_end");
static void memory(const char *phase) {
    printf("MEM,%s,internal_free=%u,internal_largest=%u\n",phase,
        (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),
        (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
    printf("PSRAM,%s,free=%u,largest=%u\n",phase,
        (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT),
        (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT));
}
extern "C" void app_main(void) {
    printf("V05,SYNTHETIC_HELDOUT_ONLY,idf=%s\nMODEL_SHA256,%s\n",esp_get_idf_version(),V05_MODEL_SHA256);
    if((size_t)(pixels_end-pixels_start)!=128U*256U||(size_t)(labels_end-labels_start)!=128U){printf("STOP,case_lengths\n");return;}
    if(!esp_psram_is_initialized()||esp_psram_get_size()!=8U*1024U*1024U){printf("STOP,requires_8MiB_PSRAM\n");return;}
    memory("before_model");const int64_t load_start=esp_timer_get_time();
    auto *model=new dl::Model((const char*)model_start,fbs::MODEL_LOCATION_IN_FLASH_RODATA);
    printf("LOAD,us=%" PRId64 "\n",esp_timer_get_time()-load_start);memory("loaded");
    /* Runs the standard exporter-stored reference, not a probability threshold. */
    const esp_err_t test=model->test();printf("EXPORT_REFERENCE_TEST,%s\n",esp_err_to_name(test));
    if(test!=ESP_OK){delete model;printf("STOP,reference_mismatch\n");return;}
    auto *input=model->get_input();auto *output=model->get_output();
    if(!input||!output||input->dtype!=dl::DATA_TYPE_INT8||output->dtype!=dl::DATA_TYPE_INT8||
       input->shape!=std::vector<int>({1,16,16,1})||(input->exponent.is_per_channel()||input->exponent.get()!=-7)||output->exponent.is_per_channel()||output->get_size()!=2){
        delete model;printf("STOP,export_contract\n");return;
    }
    unsigned confusion[2][2]={{0,0},{0,0}};
    for(unsigned n=0;n<128;n++) {
        if(labels_start[n]>1){printf("STOP,label_range\n");delete model;return;}
        auto *destination=(int8_t*)input->data;
        for(unsigned i=0;i<256;i++)destination[i]=v05_quantize_pixel(pixels_start[n*256+i]);
        const int64_t began=esp_timer_get_time();model->run();const int64_t elapsed=esp_timer_get_time()-began;
        const auto *logits=(const int8_t*)output->data;const unsigned prediction=logits[1]>logits[0]?1:0;
        confusion[labels_start[n]][prediction]++;
        printf("CASE,test-%04u,truth=%u,pred=%u,raw0=%d,raw1=%d,exponent=%d,us=%" PRId64 "\n",n,(unsigned)labels_start[n],prediction,(int)logits[0],(int)logits[1],output->exponent.get(),elapsed);
    }
    printf("SYNTHETIC_CONFUSION,circle=%u/%u,square=%u/%u\n",confusion[0][0],confusion[0][1],confusion[1][0],confusion[1][1]);
    memory("after_128");delete model;memory("released");
    printf("END,no_camera_no_GPIO_no_real_object_accuracy\n");
}

参考:[2] 固定Model API:test、run与输入输出[3] 固定模型执行/参考比较实现[4] 固定TensorBase:ExponentInfo及内存能力

08 / 小模型训练与执行验收方法

QEMU失败如实保留,诊断不能替代目标通过

固定QEMU9.2.2补充配置必须保持stdin打开、8MiB RAM并设置ssi_psram.is_octal=true;默认Quad不是OPI。恢复正确OPI后,原8通道模型在runtime3.2.0和3.3.8均参考28/执行−128,零条CASE。模型能装载、宿主退出0或固件能链接都不是目标结果正确。

保持原权重/校准/输入额外导出六层参考后,首次差异在首层Conv+ReLU NHWC[0,0,0,3]:参考0、模拟127。FbsLoader未对齐警告实际会启用param_copy,不能仅凭警告宣布根因;guest_errors也没有可指认该卷积未实现PIE的证据。另一个16通道2994参数候选虽真实训练/构建,参考77/模拟127仍失败。原因尚未确定,不能断言QEMU、转换器或实体S3有已证实缺陷。

参考:[3] 固定模型执行/参考比较实现[4] 固定TensorBase:ExponentInfo及内存能力[5] 固定FbsLoader:未对齐参数拷贝[9] 官方QEMU固定S3扩展实现;不能由失败推断未实现PIE

09 / 小模型训练与执行验收方法

从零重训:独立venv、完整脚本、新目录

本轮macOS arm64/Python3.12.13、CPU单线程,无CUDA;固定torch2.8.0、esp-ppq1.3.11、onnx1.17.0、onnxruntime1.22.1、numpy1.26.4、protobuf4.25.8、Pillow12.3.0与flatbuffers25.12.19。用tools/requirements.txt安装到独立venv,不改IDF的Python。

运行python tools/train_export.py --out NEW_TRAINING,再python tools/prepare_inputs.py --training NEW_TRAINING --out NEW_PROJECT;也可--training reference复用本轮数据。两out均必须新目录。准备器核768图/像素身份、跨分割重复、train-only校准、test顺序、模型SHA与输入格式,再复制精确manifest/锁和源,按实际训练结果刷新versions.json模型SHA/字节数及训练结果SHA,生成独立工程。

固定种子与确定性算法不能保证另一平台的浮点/序列化SHA相同,须保自己的结果/版本/算子图并重新过目标自检。最初裸Tensor导出真实失败,修为inputs=[tensor]后完整重训;原失败和后续诊断均保留,不将日志里的探索模型混为当前公开模型。

参考:[6] ESP-PPQ1.3.11固定训练后量化入口与目标[7] ESP-DL固定支持算子与量化规范

10 / 小模型训练与执行验收方法

实板验收与恢复:保存失败,门禁不让步

确认板修订、USB端口、备份与同次flasher_args后才自行idf.py -B build-v05 -p YOUR_NATIVE_USB_PORT flash monitor;原生USB会重枚举,macOS/Linux/Windows分别核实际端口。必要时按住BOOT、点RESET、松BOOT,再找端口,Ctrl+]退出monitor。本文没有执行实体烧录或推理。

预期顺序V05/MODEL_SHA、MEM/PSRAM、LOAD、EXPORT_REFERENCE_TEST;实体ESP_OK后才应出现128 CASE、SYNTHETIC_CONFUSION和END。若自检失败,保存完整log、模型/配置/ELF,停止分类与动作,再核输入/算子/版本。PSRAM失败不靠关检查绕过,panic用同次ELF。恢复采用同板原完整备份/原布局及匹配eFuse状态,不只写旧app到新factory偏移。

records表由真实板填写,包含全部失败与资源条件。QEMU us/heap不进入实体表;没有本站相机泛化、实体准确率、时延/heap、电流、能耗或续航结果。后续V10分拣还必须独立收集现实数据、验证低分拒绝与动作恢复,不能直接继承本页CPU100%。

参考:[2] 固定Model API:test、run与输入输出[3] 固定模型执行/参考比较实现[8] 固定S3-EYE指南:MB修订和原生USB

BACK TO THE SOURCE

资料来源与版本说明

固定Model/Tensor/FbsLoader/算子与PPQ接口核对;完整源、训练/PTQ/失败和同次构建证据分别留档,不把CPU图或链接成功当S3执行通过。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. ESP-DL3.3.8固定manifest与IDF范围
  2. 固定Model API:test、run与输入输出
  3. 固定模型执行/参考比较实现
  4. 固定TensorBase:ExponentInfo及内存能力
  5. 固定FbsLoader:未对齐参数拷贝
  6. ESP-PPQ1.3.11固定训练后量化入口与目标
  7. ESP-DL固定支持算子与量化规范
  8. 固定S3-EYE指南:MB修订和原生USB
  9. 官方QEMU固定S3扩展实现;不能由失败推断未实现PIE